Coroutines in C++20
首先,希望读者已经在其他语言或库中了解协程的概念。C++20 终于带来了官方的协程,这是一种无栈的协程实现。
promise / yield / return
首先来看一个例子。这段代码建议从下往上看。
#include <iostream>
#include <coroutine>
template<typename T> struct Generator {
struct promise_type;
using Handle = std::coroutine_handle<promise_type>;
struct promise_type {
T val;
Generator get_return_object() {
return Generator(Handle::from_promise(*this));
}
auto initial_suspend() {
return std::suspend_always();
}
auto final_suspend() {
return std::suspend_always();
}
void unhandled_exception() {
std::terminate();
}
auto yield_value(T &&val) {
this->val = std::move(val);
return std::suspend_always();
}
};
Generator(Generator const &) = delete;
Generator(Generator &&g) : h(g.h) {
g.h = nullptr;
}
~Generator() {
if (h) h.destroy();
}
bool go() {
return h ? (h.resume(), !h.done()) : false;
}
T& val() {
return h.promise().val;
}
private:
Handle h;
Generator(Handle h): h(h) {}
};
Generator<int> f() {
co_yield 1;
co_yield 2;
}
int main() {
auto g = f();
while (g.go()) {
std::cout << g.val() << std::endl;
}
}
当一个函数的函数体中出现了关键字 co_yield
、co_return
或 co_await
,该函数则成为一个协程函数。上面的例子中,函数 f
便是一个协程函数。协程函数中不能有 return
语句,因为协程函数中的代码是协程执行时运行的代码,而在 main
函数中的第一行 auto g = f()
,我们得到的返回值是一个 Generator
类型的生成器对象,但这并不是协程函数中的某个返回语句所返回的。总体来讲,上面的代码中,我们获得一个生成器对象 Generator g
来控制协程函数 f
的执行,可将 main
函数视为主协程,而 f
则成为其子协程。每次调用 g.go()
,会从主协程切换到子协程,协程函数 f
开始执行,直到 co_yield
语句,子协程暂停运行,切换回主协程,此时我们在主协程中调用 g.val()
来获取传给 co_yield
语句的值。再次调用 g.go()
,子协程会在上次暂停的地方恢复运行。如此,上面的代码会先后打印出 1
和 2
。
可以看到,不同于其他一些语言中由官方提供生成器实现,在 C++20 的协程中,所谓的生成器是由用户自行封装,而官方提供的是更为底层的接口,这让用户可以封装出更为灵活而丰富的实现,不局限于典型的生成器。用户自行定义一个控制协程函数执行的结构,在协程函数中声明返回此结构,此结构中必须包含一个名为 promise_type
的成员类型,是的只能叫这个名字。
promise_type
中必须定义 get_return_object
方法。形如 auto g = f()
的调用中,编译器会插入代码来构造一个协程函数的返回类型对应的 promise_type
对象,如果协程函数带有参数,则参数也会成为 promise_type
构造函数的参数。再调用其 get_return_object
方法生成一个协程函数的返回对象。这里,我们在 get_return_object
方法中让 promise_type
对象纳入到一个 std::coroutine_handle
对象中,再将 std::coroutine_handle
对象纳入到 Generator
对象中。Generator
通过 std::coroutine_handle
来控制协程的执行,其中,go
方法中调用 std::coroutine_handle
的 resume
方法让协程开始或再次运行。
promise_type
中必须定义 initial_suspend
方法。调用 get_return_object
方法后,编译器插入的代码会继续调用 initial_suspend
方法,以控制协程一开始的行为。这里,我们返回一个 std::suspend_always
对象,让协程先暂停,而在第一次调用 std::coroutine_handle
的 resume
方法后协程才真正第一次执行。如果换成 std::suspend_never
,则协程会立即开始执行,直到第一次 co_yield
之后才会暂停,从而形如 auto g = f()
的调用返回。类似的,final_suspend
方法在协程函数执行结束之后自动调用,特别的,如果这里返回 std::suspend_never
则协程继续执行从而 segfault。
如果协程函数中出现了 co_yield
语句,则 promise_type
中必须定义 yield_value
方法,来处理 co_yield
语句传过来的值,同时控制协程的执行。这里,我们将 co_yield
的值移动赋值给 promise_type
中的字段,让 Generator
来访问,并且让协程暂停。
类似的,co_return
语句也可以传一个值,此时 promise_type
中必须定义 return_value
方法,来处理 co_return
语句传过来的值,但不能在这里控制协程的执行,而是接下来由 final_suspend
方法控制。与 co_yield
语句不同,co_return
可以不传值,此时 promise_type
中必须定义 return_void
方法。如:
void return_value(T &&val) {
this->val = std::move(val);
}
void return_void() {}
协程创建时,以下对象会在堆上分配并构造:promise_type
对象、协程函数的非引用类型的形参、生命周期跨越暂停点的局部变量、一个记录协程执行情况的对象。这些对象在协程销毁时析构并释放内存。
await
co_await
是比 co_yield
和 co_return
更底层更泛化的操作。
在协程函数中,函数开头相当于插入 co_await promise.initial_suspend()
,co_yield 1;
相当于 co_await promise.yield_value(1);
,co_return 1;
相当于 promise.return_value(1); co_await promise.final_suspend(); return;
,函数结尾相当于插入 co_await promise.final_suspend(); return;
。
co_await exp
操作首先确定一个可等待对象:对于协程函数开头、结尾自动插入的,以及由 co_yield
和 co_return
而来的 co_await
操作,可等待对象为传入的表达式 exp
自身;否则,如果当前协程的 promise_type
包含 await_transform
方法,则可等待对象为 promise.await_transform(exp)
;否则,可等待对象为传入的表达式 exp
自身。然后确定一个等待器对象:如果可等待对象定义了 co_await
操作符重载,则等待器为操作符重载的返回对象,否则等待器为可等待对象自身。
例如,std::suspend_always
一种可能的实现如下:
struct suspend_always {
bool await_ready() { return false; }
void await_suspend(coroutine_handle<>) {}
void await_resume() {}
};
等待器类型必须实现以上三个方法。co_await
操作会立即调用 await_ready
方法:如果返回 true
则当前协程不暂停;否则协程暂停,返回到父协程,调用 await_suspend
方法。
await_suspend
方法传入调用 co_await
的协程的 coroutine_handle
,可返回以下三种类型:
-
void
:无其他操作,父协程继续运行。 -
bool
:true
则父协程继续运行,false
则从父协程恢复到调用co_await
的协程。 -
std::coroutine_handle
:从父协程恢复到 handle 对应的协程。
如果确定协程不暂停,或者经过暂停之后被恢复,则调用 await_resume
方法。await_resume
方法的返回值也成为 co_await
操作符的返回值。
标准库
头文件
<coroutine>
参考文档