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
", v);
}
}
显然的,这段代码会输出0-9,如果你觉得main里面的range-based-for有点玄学的话,这么写main也行。
int main() {
auto g = gen(10);
auto it = g.begin();
printf("%d
", *it); //0
it++;
printf("%d
", *it); //1
it++;
printf("%d
", *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_traits
的promise_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的值传给promise
,promise
就可以把这个值存起来,等待main
里面generator
的迭代器来取,然后yield_value
的返回值用来决定要不要暂停协程执行,我们这个例子里,同样应该暂停,回到调用者那里处理yield出来的值。
此时调用回到main
,第二行执行完毕,g
的迭代器里已经装了一个gen
函数yield出来的值。
printf
里面*it
,it
通过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销毁
}