C++20: Coroutine
Part 0. 引言
C++20引入了新的语言特性:协程(Coroutine),为C++的异步程序编写提供了新的范式,C++20采用无栈携程的设计,提高了协程程序的灵活度,但也加大了对协程程序的理解难度,本篇文章旨在带领读者了解协程的基本使用、相关概念、<coroutine>头文件的介绍。
本文中所有程序的运行结果均是在:Windows, mingw64-posix-seh-gcc10.2.0的环境下产生的
Part 1. 协程的执行
为了了解协程是如何执行的,我们从一个简单的协程程序入手:
#include <iostream>
#include <coroutine>
struct Awaitable {
bool await_ready() {
std::cout << "is not ready" << std::endl;
return false;
}
void await_suspend(std::coroutine_handle<>) {
std::cout << "suspend" << std::endl;
}
void await_resume() {
std::cout << "resumed" << std::endl;
}
};
struct Promise {
struct promise_type {
Promise get_return_object() {
std::cout << "get return object" << std::endl;
return Promise{ std::coroutine_handle<promise_type>::from_promise(*this) };
}
std::suspend_never initial_suspend() {
std::cout << "initial suspend" << std::endl;
return {};
}
std::suspend_never final_suspend() noexcept {
std::cout << "final suspend" << std::endl;
return {};
}
void unhandled_exception() {}
void return_void() {
std::cout << "return void" << std::endl;
}
};
std::coroutine_handle<promise_type> coro_handle;
};
Promise foo() {
co_await Awaiter{};
}
int main() {
auto p = foo();
p.coro_handle.resume();
return 0;
}
这个程序的输出为:
get return object
initial suspend
is not ready
suspend
resumed
return void
final suspend
上面这个程序有很多令人疑惑的点:比如为什么foo()函数没有return语句,却仍然有返回值;co_await表达式的含义;promise_type的函数为什么被调用了……等等,下面我们一一来解释。
我们需要知道的是,协程是一个函数,这个函数的返回值类型的定义内需要含有符合要求的类型promise_type;而且,这个函数内部至少含有co_await、co_yield或者co_return语句,且不能含有普通的return语句;在协程被调用(或者使用co_*语句)时,编译器会按照一定规则展开协程调用处的代码。
就上面的输出结果,这个协程是这样执行的:
首先,当一个协程开始执行时会调用operator new来创建一个coroutine state
什么是coroutine state
coroutine state包含了:
1. 一个promise_type对象
2. 按值复制的协程参数
3. 挂起点的某种表示
4. 生存期跨过当前暂停点的局部变量和临时量
然后,会构建promise_type对象
其次,调用promise_type::get_return_object作为返回值赋值给p
紧接着,调用co_await promise_type::initial_suspend()来决定是否立即执行协程
co_await promise_type::inital_suspend()中发生了什么(下文同理)
首先,会调用promise_type::initial_suspend().await_ready()来判断是否挂起协程
如果返回true,则不挂起协程,调用promise_type::initial_suspend().await_resume();并且await_resume()的返回值作为co_await expr表达式的返回值
如果返回false,调用promsise_type::initial_suspend().await_suspend;如果返回值为void/true,挂起协程;如果返回值为false,则调用promise_type::initial_suspend().await_resume;如果返回值为另一个coroutine_handle,则切换到另一个协程执行。
上面的程序中,inital_suspend()返回的是std::suspend_never,那么协程不会被挂起,开始执行协程。
执行到co_await Awaitable{};这一行,由于Awaitable::await_ready()的返回值为false,因此协程被挂起,调用void Awaitable::await_suspend(),将控制权交给协程的调用方,也就是main函数
main函数内通过p获取了协程的句柄,调用p.coro_handle.resume()来唤起协程,调用Awaitable::resume(),协程继续执行,并且co_await expr的表达式求值完毕
协程内无co_return语句(同无操作数的co_return一样),调用promise_type::return_void()
调用co_await promise_type::final_suspend(),协程结束,由于promise_type::final_suspend()返回std::suspend_never,协程不挂起,立即销毁coroutine state对象,调用std::suspend_never::await_resume()
说到这里,对协程的执行,我们应该有了一个大致的了解:promise_type是连接协程内部与协程调用者的桥梁,而awaitable决定了协程挂起和唤醒时的行为,它们内部的函数按照一定的的规则被调用,而coroutine_handle是外部操纵和控制协程的对象。
用流程图来表示:


对于一个协程的调用,编译器展开的伪代码大致如下
{
co_await promise.initial_suspend();
try
{
coroutine body;
}
catch (...)
{
promise.unhandled_exception();
}
FinalSuspend:
co_await promise.final_suspend();
}
对于co_await expr表达式,编译器展开的伪代码大致如下
{
auto&& value = <expr>;
auto&& awaitable = get_awaitable(promise, static_cast<decltype(value)>(value));
auto&& awaiter = get_awaiter(static_cast<decltype(awaitable)>(awaitable));
if (!awaiter.await_ready()) //是否需要挂起协程
{
using handle_t = std::experimental::coroutine_handle<P>;
using await_suspend_result_t =
decltype(awaiter.await_suspend(handle_t::from_promise(p)));
<suspend-coroutine> //挂起协程
if constexpr (std::is_void_v<await_suspend_result_t>)
{
awaiter.await_suspend(handle_t::from_promise(p)); //异步(也可能同步)执行task
<return-to-caller-or-resumer> //返回给caller
}
else
{
static_assert(
std::is_same_v<await_suspend_result_t, bool>,
"await_suspend() must return 'void' or 'bool'.");
if (awaiter.await_suspend(handle_t::from_promise(p)))
{
<return-to-caller-or-resumer>
}
}
<resume-point> //task执行完成,恢复协程,这里是协程恢复执行的地方
}
return awaiter.await_resume(); //返回task结果
}
Part 1+. 协程句柄,协程关键字的使用
std::coroutine_handle<P>
std::coroutine_handle<P>是我们作为协程的调用者,用于操控协程的类型。
对于std::coroutine_handle<P>,其模板参数P类型必须满足约束Promise,简单地来说,类型P的定义内需要含有promise_type类型。
std::coroutine_handle<P>可以用于恢复协程的执行,判断协程是否执行完毕,访问协程的承诺对象,以及显式地销毁协程。
- 调用
std::coroutine_handle<P>::operator ()或者std::coroutine_handle<P>::resume()可以唤起协程 - 调用
std::coroutine_handle<P>::done(),通过返回的bool值可以判断协程是否执行完毕 - 调用
std::coroutine_handle<P>::promise(),可以(以引用形式Promise&)获取协程的承诺对象 - 调用
std::coroutine_handle<P>::destory(),可以销毁协程
特别地,它的静态成员函数from_address能够从promise_type类型的对象创建一个coroutine_handle<promise_type>对象。
co_await、co_yield和co_return
本质上来讲,这三个关键字都是编译器提供的语法糖,co_await的展开形式上文中已经提到,下面主要说明后两者的展开形式
对于co_yield expr 而言,它等价于co_await promise.yield_value(expr)
对于co_return而言,当程序执行到co_return所在位置,如果表达式为co_return;,则调用promise.return_void()
否则,如果是co_return expr,则调用promise.return_value(expr),随后按照协程的正常程序终止。
Part 2. 协程相关类型的实现要求
Awaitable的实现要求
一个能被协程使用的Awaitable类型应该含有成员:
bool await_ready(),该函数决定了co_await awaiter时,是否挂起协程(返回true继续执行,返回false挂起)await_suspend(std::coroutine_handle<P>),该函数决定了挂起协程时的行为,并且- 如果返回类型为
void_t,或者返回类型为bool且返回值为true,则挂起协程,控制权转交给协程调用方 - 如果返回类型为
bool且返回值为false,则取消挂起协程 - 如果返回类型为
coroutine_handle<P>,则挂起协程,控制权转交给coroutine_handle<P>指定的协程
- 如果返回类型为
await_resume(),该函数决定了唤起协程时的行为,且- 如果返回类型为
void,则co_await awaiter表达式无返回值 - 如果返回类型为
T,则co_await awaiter表达式的返回值为T类型的对象
- 如果返回类型为
promise_type的实现要求
一个能被协程使用的promise_type应该至少含有成员:
R get_return_object(),该函数决定了协程调用处获得的返回值awaitable initial_suspend(),该函数决定了协程启动时的行为awaitable final_suspend(),该函数决定了协程结束,或者协程出现异常被迫停止后的行为void unhandled_exception() noexcept,该函数决定了协程执行过程中出现未捕获的异常后的行为void return_void()或者void return_value(T),用于决定co_return expr的行为。这两个函数在同一promise_type中只能存在一个
另外地,还可以添加以下成员用以扩展协程功能:
std::suspend_always yield_value(T),用于决定co_yield expr的行为。需要注意的是,co_yield只是一种语法糖形式,本质上是调用了yield_value(T),如果该函数返回的awaiter没有指示协程挂起,那么与co_yield expr的预期语义相悖。R get_return_object_on_allocation_failure(),该函数用于在coroutine state分配失败时调用,并且作为协程调用处的返回值A await_transform(T),该函数用于co_await expr中,将expr的求值结果转换为特定的awaitable类型。需要注意的是,如果await_transform(expr)的求值结果类型定义了operator co_await(T),那么优先调用await_transform,以获得awaitable类型,然后调用operator co_await以获得awaiter,否则,以awaitable本身作为awaiter。
Part 3. 协程的异常处理(等待更新)
Part 4. 有关协程的细节和注意事项(等待更新)
0x00000001: 协程创建、销毁时,构造、析构函数的调用