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: 协程创建、销毁时,构造、析构函数的调用