C++ coroutine-ts 怎么用-Part 3 从generator看co_yield怎么用

clang和MSVC的最新实现已经提供了试验性的协程实现,想要试用的话,clang须要开启-fcoroutins-ts -stdlib=libc++两个开关,而MSVC须要开启/await开关。c++

咱们从一个简单的generator来看编译器作了什么:promise

#include <experimental/generator>
#include <cstdio>

namespace stdexp = std::experimental;

stdexp::generator<int> gen(int count) {
    for (int i = 0; i < count; i++) {
        co_yield i;
    }
}

int main() {
    for (auto v : gen(10)) {
        printf("%d\n", v);
    }
}

显然的,这段代码会输出0-9,若是你以为main里面的range-based-for有点玄学的话,这么写main也行。函数

int main() {
    auto g = gen(10);
    auto it = g.begin();
    printf("%d\n", *it); //0
    it++;
    printf("%d\n", *it); //1
    it++;
    printf("%d\n", *it); //2
}

第一次调用gen,生成了一个generator g,而后咱们获取了g的迭代器,每次对迭代器取值都能得到一个从gen函数里yield出来的值,而对迭代器的++,不难猜出,它负责恢复gen函数的执行。spa

若是你为所欲为的删除或者增长代码里的it++,就能发现输出的结果会响应的跳过一些值,不难猜出,每次经过it++恢复gen函数执行,gen函数yield以后,都会把yield的值存在某个地方,而后你才能经过*it的操做把它取出来。设计

promise类型

这个某个地方,其实就是promise类型和返回值的共同做用结果,对于每一个协程,在正式进入函数体时,编译器都会构造一个promise_type的对象,这个promise_type按照C++的尿性,必然是你本身提供的,经过一个traits,这个咱们后面会讲。而后编译器会调用promise的get_return_object方法,这个方法的返回值在本例中会被用来构造generator对象,也就是说coroutine的返回值是经过promise提供的,为何要这样设计,由于coroutine常常须要在每次suspend/resume以前/后修改或设置返回值,好比本例的存一个int进去。因此须要让promise和返回值之间创建一个联系,这样最好的方式就让promise来提供返回值,这样promise_type和return_type的内部实现之间就能够搞一些py交易,好比互相保存一下对方的指针什么的,来实现互相操做对方。指针

这么多话,总结一下,就是coroutine经过操做promise来修改返回值,promise是coroutine向外返回的结果的入口。code

同时promise_type也是整个协程的抽象,最先咱们提到过编译器实现的无栈协程会把整个协程分配在某个地方,而这里的promise,就成为了编译器向你暴露的协程的一个接口。协程

函数究竟是怎么执行的

最开始,编译器会构造gen函数的promise_type对象,经过coroutine_traitspromise_type类型。coroutine_traits接受函数返回类型和参数类型做为参数,你能够本身特化它。对象

stdexp::coroutine_traits<stdexp::generator<int>, int>::promise_type __p;

咱们这个例子里返回类型是generator<int>,有一个参数int接口

而后编译器调用__p.get_return_object(),用他的返回值(多半是这个promise自己)来构造main里的generator g

g的构造函数会经过(编译器)标准库提供的coroutine_handle<promise_type>::from_promise(__p),从promise获取对应的coroutine_handle,这是表明协程的句柄,用它来控制协程的恢复和销毁。

而后编译器调用__p.initial_suspend(),它的返回值用来决定要不要在函数体执行前暂停,在咱们这个例子里,应该暂停,由于函数体应当在第一次获取迭代器(即调用begin时开始执行)。

而后函数被暂停,调用回到main,第一行执行完毕。

当你调用g,即那个generator<int>begin后,g__handle.resume()恢复协程执行,gen函数进入循环体。

gen函数第一次co_yield时,编译器调用__p.yield_value(i),将局部变量i的值传给promisepromise就能够把这个值存起来,等待main里面generator的迭代器来取,而后yield_value的返回值用来决定要不要暂停协程执行,咱们这个例子里,一样应该暂停,回到调用者那里处理yield出来的值。

此时调用回到main,第二行执行完毕,g的迭代器里已经装了一个gen函数yield出来的值。

printf里面*itit经过generator里面存的coroutine_handle把值取出来,coroutine_handle能够经过promise成员函数来获取到对应的promise

it++一样也会恢复协程执行,和上面begin的描述同样。

当你第11次调用it++时,gen函数其实是第12次恢复执行(由于begin恢复执行了一次),gen函数从循环最后一次yield的位置恢复,退出循环,执行到函数体的结尾,此时编译器会调用__p.final_suspend(),询问你是否是要在最后再暂停一次gen函数,咱们这里是须要的。由于在控制流从gen的循环退出,执行到函数最后时,若是协程直接结束,coroutine_handle直接被销毁,it无从得知它的__handle已经变为野指针,当他用__handle.done()来更新本身是否到end的状态时就会爆炸,因此最后应该再额外暂停一次,就像迭代器容许指向一个尾后位置来表示end同样,容许一个协程在函数体执行完毕,销毁以前再暂停一下,表示一个end状态。

main最后generator g销毁时会顺带执行__handle.destroy(),销毁gen协程。

用伪代码说一下

generator<int> gen(int a) {
    coroutine_traits<generator<int>, int>::promise_type __p;
    generator<int> __r = __p.get_return_object(); //这里其实是构造了main里的返回值,可是代码里无法描述,就写在gen里了
    if (__p.initial_suspend()) { //true
        //第1次暂停
    }
    for (int i = 0; i < count; i++) {
        if (__p.yield_value(i)) { //true
            //第i+2次暂停
        }
    }
    if (__p.final_suspend()) { //true
        //第count+2次暂停
    }
    //__p销毁
}
相关文章
相关标签/搜索