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

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

我们从一个简单的generator来看编译器做了什么:

#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函数的执行。

如果你随心所欲的删除或者增加代码里的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向外返回的结果的入口。

同时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销毁
}

原文链接: https://www.cnblogs.com/pointer-smq/p/8877635.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

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

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/272553

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月14日 下午10:49
下一篇 2023年2月14日 下午10:50

相关推荐