C++20协程
C++20协程
简介
C++20协程只是提供协程机制,而不是提供协程库。C++20的协程是无栈协程,无栈协程是一个可以挂起/恢复的特殊函数,是函数调用的泛化,且只能被线程调用,本身并不抢占内核调度。
C++20 提供了三个新关键字(co_await、co_yield 和 co_return),如果一个函数中存在这三个关键字之一,那么它就是一个协程。
协程相关的三个关键字:co_await、co_yield与co_return
- co_yield some_value: 保存当前协程的执行状态并挂起,返回some_value给调用者
- co_await some_awaitable: 如果some_awaitable没有ready,就保存当前协程的执行状态并挂起
- co_return some_value: 彻底结束当前协程,返回some_value给协程调用者
协程相关的对象
协程帧(coroutine frame)
当 caller 调用一个协程的时候会先创建一个协程帧,协程帧会构建 promise 对象,再通过 promise 对象产生 return object。
协程帧中主要有这些内容:
-
协程参数
-
局部变量
-
promise 对象
这些内容在协程恢复运行的时候需要用到,caller 通过协程帧的句柄 std::coroutine_handle 来访问协程帧。
promise_type
promise_type 是 promise 对象的类型。promise_type 用于定义一类协程的行为,包括协程创建方式、协程初始化完成和结束时的行为、发生异常时的行为、如何生成 awaiter 的行为以及 co_return 的行为等等。promise 对象可以用于记录/存储一个协程实例的状态。每个协程桢与每个 promise 对象以及每个协程实例是一一对应的。
promise_type的接口
promise_type接口 | 功能 |
---|---|
initial_suspend() | 控制协程初始化完成后是否挂起 |
final_suspend() | 控制协程执行完后是否挂起 |
get_return_object() | 返回给 caller 一个对象 |
unhandled_exception() | 处理异常 |
return_void() | 调用co_return;时或者协程执行完后被调用 |
return_value(T) | 保存协程返回值。调用co_return xxx;的时候被调用,保存协程返回值 |
yield_value() | 调用co_yield xxx;的时候,会调用,保存协程返回值 |
await_transform() | 用于定制协程body中co_await xxx;语句的行为。定义该方法后,编译器会将出现在协程主体中的每个co_await xxx;转换为co_await promise.await_transform(xxx) |
promise_type里的接口需要我们实现,promise_type里的接口是给编译器调用的
coroutine return object
它是promise.get_return_object()方法创建的,一种常见的实现手法会将 coroutine_handle 存储到 coroutine object 内,使得该 return object 获得访问协程的能力。
std::coroutine_handle
协程帧的句柄,主要用于访问底层的协程帧、恢复协程和释放协程帧。
程序员可通过调用 std::coroutine_handle::resume() 唤醒协程。
std::coroutine_handle接口
coroutine_handle接口 | 作用 |
---|---|
from_promise() | 从promise对象创建一个coroutine_handle |
done() | 检查协程是否运行完毕 |
operator bool | 检查当前句柄是否是一个coroutie |
operator() | 恢复协程的执行 |
resume | 恢复协程的执行(同上) |
destroy | 销毁协程 |
promise | 获取协程的promise对象 |
address | 返回coroutine_handle的指针 |
from_address | 从指针导入一个coroutine_handle |
coroutine_handle的接口不需要我们实现,可以直接调用
co_await、awaiter、awaitable
-
co_await:一元操作符;
-
awaitable:支持 co_await 操作符的类型;
-
awaiter:定义了 await_ready、await_suspend 和 await_resume 方法的类型。
co_await expr(expr是表达式) 通常用于表示等待一个任务(可能是 lazy 的,也可能不是)完成。co_await expr 时,expr 的类型需要是一个 awaitable,而该 co_await表达式的具体语义取决于根据该 awaitable 生成的 awaiter。
协程对象如何协作
以一个简单的代码展示这些协程对象如何协作:
Return_t foo () {
auto res = co_await awaiter;
co_return res ;
}
Return_t:promise return object。
awaiter: 等待一个task完成。
图中浅蓝色部分的方法就是 Return_t 关联的 promise 对象的函数,浅红色部分就是 co_await 等待的 awaiter。
这个流程的驱动是由编译器根据协程函数生成的代码驱动的,分成三部分:
- 协程创建;
- co_await awaiter 等待 task 完成;
- 获取协程返回值和释放协程帧。
协程的创建
Return_t foo () {
auto res = co_await awaiter;
co_return res ;
}
foo()协程会生成下面这样的模板代码(伪代码),协程的创建都会产生类似的代码:
{
co_await promise.initial_suspend();
try
{
coroutine body;
}
catch (...)
{
promise.unhandled_exception();
}
FinalSuspend:
co_await promise.final_suspend();
}
首先需要创建协程,创建协程之后是否挂起则由调用者设置 initial_suspend 的返回类型来确定。
创建协程的流程大概如下:
- 创建一个协程帧(coroutine frame)
- 在协程帧里构建 promise 对象
- 把协程的参数拷贝到协程帧里
- 调用 promise.get_return_object() 返回给 caller 一个对象,即代码中的 Return_t 对象
在这个模板框架里有一些可定制点:如 initial_suspend、final_suspend、unhandled_exception 和 return_value。
我们可以通过 promise 的 initial_suspend 和 final_suspend 返回类型来控制协程是否挂起,在 unhandled_exception 里处理异常,在 return_value 里保存协程返回值。
可以根据需要定制 initial_suspend 和 final_suspend 的返回对象来决定是否需要挂起协程。如果挂起协程,代码的控制权就会返回到caller,否则继续执行协程函数体(function body)。
PS:如果禁用异常,那么生成的代码里就不会有 try-catch。此时协程的运行效率几乎等同非协程版的普通函数。
co_await 机制
co_await 操作符是 C++20 新增的一个关键字,co_await expr 一般表示等待一个惰性求值的任务,这个任务可能在某个线程执行,也可能在 OS 内核执行,什么时候执行结束不知道,为了性能,我们又不希望阻塞等待这个任务完成,所以就借助 co_await 把协程挂起并返回到 caller,caller 可以继续做事情,当任务完成之后协程恢复并拿到 co_await 返回的结果。
所以 co_await 一般有这几个作用:
- 挂起协程;
- 返回到 caller;
- 等待某个任务(可能是 lazy 的,也可能是非 lazy 的)完成之后返回任务的结果。
编译器会根据 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结果
}
这个代码执行流程就是“协程运行流程图”中粉红色部分,从这个生成的代码可以看到,通过定制 awaiter.await_ready() 的返回值就可以控制是否挂起协程还是继续执行,返回 false 就会挂起协程,并执行 awaiter.await_suspend,通过 awaiter.await_suspend 的返回值来决定是返回 caller 还是继续执行。
正是 co_await 的这种机制是变“异步回调”为“同步”的关键。
C++20 协程中最重要的两个对象就是 promise 对象(恢复协程和获取某个任务的执行结果)和 awaiter(挂起协程,等待task执行完成),其它的都是“工具人”,要实现想要的的协程,关键是要设计如何让这两个对象协作好。
例子
#include <iostream>
#include <coroutine>
#include <thread>
namespace Coroutine {
struct task {
struct promise_type {
promise_type() {
std::cout << "1.task-promise_type():create promise object\n";
}
task get_return_object() {
std::cout << "2.task-get_return_object():create coroutine return object, and the coroutine is created now\n";
return {std::coroutine_handle<task::promise_type>::from_promise(*this)};
}
// initial_suspend()决定协程初始化后,是继续直接继续执行协程,还是挂起协程返回caller
// 返回std::suspend_never,表示不挂起协程,会继续执行协程函数体(coroutine body)
// 返回std::suspend_always,表示挂起协程,不会去执行coroutine body,程序的执行返回到caller那里
std::suspend_never initial_suspend() {
std::cout << "3.task-initial_suspend():do you want to susupend the current coroutine?\n";
std::cout << "4.task-initial_suspend():don't suspend because return std::suspend_never, so continue to execute coroutine body\n";
return {};
}
//也可以写成这样
//auto initial_suspend() { return std::suspend_never{}; }
// 调用完void return_void()或者void return_value(T v)后,就会调用final_suspend()
// 如果final_suspend返回std::suspend_never表示不挂起协程,那么协程就会自动销毁,先后销毁promise, 协程帧上得参数和协程帧;
// 如果返回std::suspend_always则不会自动销毁协程,需要用户手动去删除协程。
std::suspend_never final_suspend() noexcept {
std::cout << "15.task-final_suspend():coroutine body finished, do you want to susupend the current coroutine?\n";
std::cout << "16.task-final_suspend():don't suspend because return std::suspend_never, and the continue will be automatically destroyed, bye\n";
return {};
}
// 如果协程是void没有返回值,那么就需要定义void return_void()
// 如果有返回值那么就定义void return_value(T v),用来保存协程的返回值
// return_value或者return_void,这两个方法只允许存在一个
void return_void() {
std::cout << "14.task-return_void():coroutine don't return value, so return_void is called\n";
}
void unhandled_exception() {}
};
std::coroutine_handle<task::promise_type> handle_;
};
struct awaiter {
// 调用co_wait awaiter{};时调用await_ready()
// 表示是否准备好,要不要挂起协程
// await_ready()返回false一般表示要挂起协程,并执行await_suspend
// 返回true说明协程已经执行完了,这时候调用await_resume返回协程的结果。
bool await_ready() {
std::cout << "6.await_ready():do you want to suspend current coroutine?\n";
std::cout << "7.await_ready():yes, suspend becase awaiter.await_ready() return false\n";
return false;
}
//await_suspend 的返回值来决定是返回 caller 还是继续执行。
//返回void:协程执行权交还给当前协程的caller。当前协程在未来某个时机被resume之后,然后执行协程函数中co_await下面的语句
//返回true:同返回void。
//返回false:直接执行await_resume
void await_suspend(std::coroutine_handle<task::promise_type> handle) {
std::cout << "8.await_suspend(std::coroutine_handle<task::promise_type> handle):execute awaiter.await_suspend()\n";
std::thread([handle]() mutable {
std::cout << "11.lambada():resume coroutine to execute coroutine body\n";
handle();//等价于handle.resume();
std::cout << "17.lambada():over\n";
}).detach();
std::cout << "9.await_suspend(std::coroutine_handle<task::promise_type> handle):a new thread lauched, and will return back to caller\n";
}
//调用完await_resume后直接执行协程函数中co_await下面的语句
void await_resume() {
std::cout << "12.await_resume()\n";
}
};
task test() {
std::cout << "5.test():begin to execute coroutine body, the thread id=" << std::this_thread::get_id() << ",and call co_await awaiter{};\n"; //#1
co_await awaiter{};
std::cout << "13.test():coroutine resumed, continue execute coroutine body now, the thread id=" << std::this_thread::get_id() << "\n"; //#3
}
template<typename T>
struct lazy {
public:
struct promise_type;
lazy(std::coroutine_handle<promise_type> handle) : m_handle(handle) {
std::cout << "3.lazy(std::coroutine_handle<promise_type> handle):Construct a lazy object" << std::endl;
}
~lazy() {
std::cout << "15.~lazy():Destruct a lazy object " << std::endl;
m_handle.destroy();
}
T get() {
std::cout << "6.lazy.get():I want to execute the coroutine now. call m_handle.resume()" << std::endl;
if (!m_handle.done()) {
m_handle.resume();
}
std::cout << "13.lazy.get():We got the return value...:" << m_handle.promise().value << std::endl;
return m_handle.promise().value;
}
struct promise_type {
T value = {};
promise_type() {
std::cout << "1.lazy-promise_type():Promise created" << std::endl;
}
~promise_type() {
std::cout << "16.lazy- ~promise_type():Promise died" << std::endl;
}
auto get_return_object() {
std::cout << "2.lazy-get_return_object():create coroutine return object, and the coroutine is created now" << std::endl;
return lazy<T>{std::coroutine_handle<promise_type>::from_promise(*this)};
}
auto initial_suspend() {
std::cout << "4.lazy-initial_suspend():Started the coroutine" << std::endl;
return std::suspend_always{};
}
auto final_suspend() noexcept {
std::cout << "12.lazy-final_suspend():Finished the coroutine" << std::endl;
return std::suspend_always{};
}
void return_value(T v) {
std::cout << "11.lazy-return_value(T v):Got coroutine result " << v << std::endl;
value = v;
}
void unhandled_exception() {
std::exit(1);
}
//协程体中调用co_yield xxx;的时候调用yield_value(T val)
auto yield_value(T val) {
std::cout << "9.lazy-yield_value(T val): " << val << std::endl;
value = val;
//后续不再挂起协程,继续执行
return std::suspend_never();
// //后续继续挂起协程
// return std::suspend_always();
}
};
std::coroutine_handle<promise_type> m_handle;
};
lazy<int> my_coroutine() {
std::cout << "7.my_coroutine():Execute the coroutine function body" << std::endl;
std::cout << "8.my_coroutine():call---co_yield 66;" << std::endl;
co_yield 66;
std::cout << "10.my_coroutine():call---co_return 88;" << std::endl;
co_return 88;
}
} // namespace Coroutine
int main() {
Coroutine::test();
std::cout << "10.main():come back to caller becuase of co_await awaiter\n";
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "-----------------------------------" << std::endl;
auto coro = Coroutine::my_coroutine();
std::cout << "5.main():call coro.get()" << std::endl;
auto result = coro.get();
std::cout << "14.main():The coroutine result: " << result << std::endl;
// std::cout << "main():Second call coro.get() " << std::endl;
// result = coro.get();
// std::cout << "main():The coroutine result2: " << result << std::endl;
return 0;
}
输出结果:
1.task-promise_type():create promise object
2.task-get_return_object():create coroutine return object, and the coroutine is created now
3.task-initial_suspend():do you want to susupend the current coroutine?
4.task-initial_suspend():don't suspend because return std::suspend_never, so continue to execute coroutine body
5.test():begin to execute coroutine body, the thread id=1,and call co_await awaiter{};
6.await_ready():do you want to suspend current coroutine?
7.await_ready():yes, suspend becase awaiter.await_ready() return false
8.await_suspend(std::coroutine_handle<task::promise_type> handle):execute awaiter.await_suspend()
9.await_suspend(std::coroutine_handle<task::promise_type> handle):a new thread lauched, and will return back to caller
10.main():come back to caller becuase of co_await awaiter
11.lambada():resume coroutine to execute coroutine body
12.await_resume()
13.test():coroutine resumed, continue execute coroutine body now, the thread id=2
14.task-return_void():coroutine don't return value, so return_void is called
15.task-final_suspend():coroutine body finished, do you want to susupend the current coroutine?
16.task-final_suspend():don't suspend because return std::suspend_never, and the continue will be automatically destroyed, bye
17.lambada():over
-----------------------------------
1.lazy-promise_type():Promise created
2.lazy-get_return_object():create coroutine return object, and the coroutine is created now
3.lazy(std::coroutine_handle<promise_type> handle):Construct a lazy object
4.lazy-initial_suspend():Started the coroutine
5.main():call coro.get()
6.lazy.get():I want to execute the coroutine now. call m_handle.resume()
7.my_coroutine():Execute the coroutine function body
8.my_coroutine():call---co_yield 66;
9.lazy-yield_value(T val): 66
10.my_coroutine():call---co_return 88;
11.lazy-return_value(T v):Got coroutine result 88
12.lazy-final_suspend():Finished the coroutine
13.lazy.get():We got the return value...:88
14.main():The coroutine result: 88
15.~lazy():Destruct a lazy object
16.lazy- ~promise_type():Promise died
Process finished with exit code 0
参考:
1.https://blog.csdn.net/csdnnews/article/details/124123024
2.http://purecpp.org/detail?id=2270
3.http://purecpp.org/detail?id=2278
4.http://purecpp.org/detail?id=2275