声明:仅为个人学习总结,还请批判性查看,如有不同观点,欢迎交流。
摘要
本文结合协程示例源码,和编译器转换后的代码,对 C++ 20 协程的构成,及其执行流程进行分析和整理。
目录
1 C++20 协程概述
1.1 概念说明
cppreference 说明:
A coroutine is a function that can suspend execution to be resumed later.
协程是一种特殊的函数,可以在执行过程中挂起(并保存当前的状态),稍后可以恢复执行。
Coroutines are stackless: they suspend execution by returning to the caller and the data that is required to resume execution is stored separately from the stack.
协程是无栈的:它们通过返回到调用者来挂起执行,与传统的函数调用不同,协程在挂起时不会使用调用栈来保存状态,而是将恢复执行所需的数据与栈数据分开存储。
This allows for sequential code that executes asynchronously (e.g. to handle non-blocking I/O without explicit callbacks), and also supports algorithms on lazy-computed infinite sequences and other uses.
协程的特性使得代码可以“以顺序的方式编写,以异步的方式执行”(例如,在处理非阻塞 I/O 操作时,并不需要显式的回调函数);此外,协程还支持针对无限序列的惰性计算算法,以及其他的用途。
结合 协程1-并发基础概念 => 4 协程 部分的介绍,也许可以帮助我们更好的理解上述说明,以及本文后续内容。
1.2 协程函数
协程函数需要满足的条件包括:
- 函数体至少包含下表某一关键字:
序号 关键字 功能 代码示例 1 co_await
co_await a
表达式:用于挂起执行,直到被恢复。a
的类型必须 满足一组指定的要求co_await async_write(socket, buffer);
2 co_yield
co_yield v
表达式:用于挂起执行,并返回一个值。co_yield
是co_await
的语法糖co_yield n++;
3 co_return
co_return
语句:用于结束执行,并可以返回一个值。co_return 7;
- 函数返回类型必须 满足一组指定的要求;
- 函数的其它限制:
- 不能使用变长实参、普通的
return
语句、占位符返回类型(auto
或Concept
); consteval
函数、constexpr
函数、构造函数、析构函数、main
函数不能作为协程。
- 不能使用变长实参、普通的
根据以上信息可以判断,理解协程的两个重点分别为:协程返回类型 和 co_await
表达式。
协程交互示意:
1.3 编译环境
可以从 cppreference 查看 Coroutines
的编译器支持情况。
本文使用的编译器是 GCC 10.5.0,对应的编译指令为:
g++ -fcoroutines -std=c++20
2 协程返回类型
2.1 示例代码
下面是在 协程1-并发基础概念 中提到的“斐波那契数列生成器”的完整代码,其中除了之前提到的 main
函数和 fibo
协程函数,还包括协程返回类型 generator
的定义。
// 编译指令:g++ -fcoroutines -std=c++20 demo_gen.cc
#include <coroutine>
#include <iostream>
#include <stdexcept>
template <typename T>
struct generator {
// 类名 `generator` 不是必须的,可以使用 'MyGenerator' (或者任何别的名字)替代,
// 同时在嵌套类 `promise_type` 中包含 'MyGenerator get_return_object()' 方法。
struct promise_type;
using handle_type = std::coroutine_handle<promise_type>;
struct promise_type {
T value_; // 缓存协程执行结果
std::exception_ptr exception_; // 缓存协程执行异常
generator get_return_object() { return generator(handle_type::from_promise(*this)); }
std::suspend_never initial_suspend() noexcept { return {}; } // 如果计算复杂,或考虑异常,建议返回 suspend_always
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { exception_ = std::current_exception(); } // 保存异常
template <std::convertible_to<T> From> // C++20 concept
std::suspend_always yield_value(From&& arg) {
value_ = std::forward<From>(arg); // 在协程内部执行,将结果缓存到 promise 中
return {};
}
void return_void() {}
};
generator() = default;
generator(generator&& src) : handle_(src.handle_) { src.handle_ = nullptr; }
generator& operator=(generator&& src) {
if (this != &src) {
if (handle_) {
handle_.destroy();
}
handle_ = src.handle_;
src.handle_ = nullptr;
}
return *this;
}
~generator() {
if (handle_) {
handle_.destroy();
}
}
private:
handle_type handle_;
generator(handle_type handle) : handle_(handle) {}
void check() {
if (!handle_) {
throw std::logic_error("coroutine handle is null");
}
}
public:
T value() {
check();
return std::move(handle_.promise().value_); // 在协程外部执行,从 promise 中“移走”结果
}
bool done() {
check();
return handle_.done();
}
void resume() {
check();
handle_.resume();
if (handle_.promise().exception_) {
std::rethrow_exception(handle_.promise().exception_);
}
}
};
// 协程函数,生成斐波那契数列 1,1,2,3,5,8...
generator<unsigned> fibo(int n) {
unsigned a = 0, b = 1;
for (int i = 0; i < n; i++) {
co_yield b; // 每次生成 1 个数值,挂起,等待恢复
auto next = a + b;
a = b;
b = next;
}
co_return; // 可省略
}
int main() {
constexpr int count = 6;
auto f = fibo(count); // 调用 fibo 协程
for (; !f.done(); f.resume()) { // 如果没有完成,恢复执行
std::cout << f.value() << std::endl; // 每次获得 1 个数值
}
}
2.2 返回类型结构
示例代码中的协程返回类型定义包括三部分:返回类型 generator<T>
、承诺类型 generator::promise_type
、协程句柄 coroutine_handle<promise_type>
。
2.2.1 协程句柄
为了在恢复协程时,可以从挂起时的状态继续执行,通常会在堆中创建协程帧,其中保存了协程恢复和协程销毁两个函数指针,也保存了协程实参、内部状态和局部变量等数据,其中承诺(Promise)对象的类型 promise_type
需要自定义实现。
协程句柄 coroutine_handle
是标准库提供的模板类,它封装了协程帧的指针,并提供恢复协程、销毁协程,以及获取协程的 Promise 对象等接口。
协程句柄用于在“协程外部”进行操作,并且不带所有权(non-owning),可以自由的进行转移。
// 头文件:include/c++/10.5.0/coroutine
template <typename _Promise = void>
struct coroutine_handle; // 类模板
template <>
struct coroutine_handle<void> // 类型擦除版本,忽略 Promise 对象,只关注协程的恢复、销毁以及句柄的传递等
{
constexpr coroutine_handle() noexcept; // 默认构造函数,协程帧指针初始化为空
constexpr coroutine_handle(std::nullptr_t __h) noexcept; // 带参构造函数,通过参数初始化协程帧指针为空
coroutine_handle& operator=(std::nullptr_t) noexcept; // 将协程帧指针赋值为空(nullptr)
constexpr void* address() const noexcept; // 获取协程帧地址/指针
constexpr static coroutine_handle from_address(void* __a) noexcept; // 通过协程帧指针创建协程句柄
constexpr explicit operator bool() const noexcept; // 判断协程帧指针是否为空
bool done() const noexcept; // 判断协程是否执行完成
void operator()() const; // 恢复协程执行,内部调用 resume()
void resume() const; // 对一个挂起后的协程进行恢复,在该协程再次挂起后,resume 接口得以返回
void destroy() const; // 销毁协程帧
protected:
void* _M_frame_ptr; // 协程帧指针
};
template <typename _Promise> // 携带 Promise 类型信息
struct coroutine_handle : coroutine_handle<>
{
using coroutine_handle<>::coroutine_handle;
static coroutine_handle from_promise(_Promise& p); // 为 _Promise 对象创建协程帧,通过协程帧指针构造协程句柄
coroutine_handle& operator=(std::nullptr_t) noexcept; // 将协程帧指针赋值为空(nullptr)
constexpr static coroutine_handle from_address(void* __a) noexcept; // 通过协程帧指针创建协程句柄
_Promise& promise() const; // 获取协程帧所对应的 _Promise 对象
};
2.2.2 承诺对象
在计算机科学中,Promise(承诺)概念用于描述一个未知值的对象,生产者可以通过 Promise 对象提供一个值(如示例代码中的
promise_type::yield_value()
),而消费者通过对应的 Future 对象来获取将来的值(如示例代码中的generator::value()
)。
在默认情况下,编译器会查找协程返回类型 R
的嵌套类型成员 R::promise_type
作为 Promise,这个类型需要实现几个特定的接口:
序号 | 方法/属性 | 是否必需 | 说明 |
---|---|---|---|
1 | T value_ | 否 | 缓存协程执行结果 |
2 | exception_ptr exception_ | 否 | 缓存协程执行异常信息 |
3 | R get_return_object() | 是 | 构造外部类型 R 的实例(协程的返回值),该实例将在第一次挂起后返回给协程的调用者。 |
4 | awaiter initial_suspend() | 是 | 自定义在执行协程的第一行代码之前是否需要挂起协程,如果挂起,后续可以通过协程句柄恢复,然后开始执行第一行代码,或者也可以在挂起后销毁协程。 |
5 | awaiter final_suspend() noexcept | 是 | 自定义在协程结束之前是否需要挂起,如果挂起,此时 coroutine_handle::done() 接口为真,后续只能通过协程句柄去手动销毁;如果不挂起,将自动销毁协程。 |
6 | void unhandled_exception() | 是 | 处理协程体内发生的异常,一般会通过调用 std::current_exception() 来获取并存储异常的副本,以便后续当程序从 Future 取值时进行重抛。 |
7 | awaiter yield_value(From&& arg) | co_yield 相关 | 自定义 co_yield 的行为, co_yield v 会被转换成 co_await promise.yield_value(v) ,通常生成器使用这个接口产生一个值,并将控制权返回给它的调用者或恢复者。 |
8 | void return_void() void return_value(Arg) | 二选一 | 自定义 co_return; 或 co_return expr; 的行为, 编译器将其转换成对 return_void() 或 return_value(expr) 接口的调用;此时协程即将结束它的生命周期。 |
9 | template <typename Obj> promise_type(Obj&&, Args...) promise_type(Args...) promise_type() | 否 | 不同参数类型的构造函数。 如果协程形参与 promise_type 构造函数一致,那么将会使用重载形参后的构造函数,否则使用默认的构造函数。对于成员函数/lambda,第一个参数类型需要与 class/lambda 类型一致。 |
10 | awaitable await_transform(expr) | 否 | 自定义协程体内的 co_await 表达式,co_await 表达式会首先通过这个接口来得到 awaitable 对象,然后再将 awaitable 对象转换为 awaiter 对象。 |
部分方法的返回类型 awaiter
,是 co_await
运算符对应的操作数类型,会在 3 co_await 表达式 部分,进行展开说明。
示例代码中使用的 std::suspend_never
和 std::suspend_always
是标准库提供的两个 awaiter
,分别代表“不挂起”和“挂起”协程。
Promise 类型能够让程序定制一个协程在初始化时和结束前的行为,以及协程体内的 co_await
、co_yield
与 co_return
表达式的行为。
Promise 对象用于在“协程内部”进行操作,协程通过此对象提交其结果和异常信息。
2.2.3 返回类型
返回类型 generator<T>
成员说明:
序号 | 成员 | 是否必需 | 说明 |
---|---|---|---|
0 | generator::promise_type | 是 | 协程返回类型需要包含嵌套的 promise_type 类型定义,其它成员可以根据实际需要实现。如果无法为已存在的类型添加类型成员,可以通过为 coroutine_traits 元函数提供特化版本来扩展其 promise_type 。 |
1 | generator() | 否 | 默认构造函数,协程句柄初始为空 |
2 | generator(generator&&) | 否 | 移动构造函数,通过移动语义来保证协程句柄的安全传递 |
3 | generator& operator=(generator&&) | 否 | 移动赋值运算符,同上 |
4 | ~generator() | 否 | 通过析构函数释放协程句柄,因为 promise_type::final_suspend() 返回的是 std::suspend_always |
5 | handle_type handle_ | 否 | 协程句柄 |
6 | generator(handle_type) | 否 | 参数为协程句柄的构造函数,用于通过 Promise 对象 promise_type::get_return_object() 创建协程返回对象 |
7 | void check() | 否 | 检查协程句柄的有效性 |
8 | T value() | 否 | 获取协程执行结果 |
9 | bool done() | 否 | 判断协程是否完成执行 |
10 | void resume() | 否 | 恢复协程执行 |
协程返回类型 generator<T>
,不仅可以通过 value()
等接口访问协程数据,还会管理协程的生命周期,通过析构函数释放协程句柄,是一个典型的 RAII 类。
2.3 生成器处理流程
2.3.1 编译转换代码
在协程函数体及返回类型符合要求后,编译器会对协程进行代码转换。本节整理了通过 cppinsights.io 得到的转换后的代码(仅为示意)。
/*************************************************************************************
* NOTE: The coroutine transformation you've enabled is a hand coded transformation! *
* Most of it is _not_ present in the AST. What you see is an approximation. *
*************************************************************************************/
C++ Insights 是一个基于 clang 的代码转换工具,可以查看编译器对代码进行的一些转换(transformation)。本文所使用的版本为:cpp-insights 17.0(Commits on Dec 6, 2023)、Ubuntu Clang 17.0.2。
转换前的协程代码:
generator<unsigned> fibo(int n) {
unsigned a = 0, b = 1;
for (int i = 0; i < n; i++) {
co_yield b;
auto next = a + b;
a = b;
b = next;
}
co_return;
}
转换后的代码分为四部分,为别为:
- 协程帧定义:
struct __fiboFrame;
- 协程函数:
generator<unsigned int> fibo(int n);
- 协程恢复函数:
void __fiboResume(__fiboFrame *__f);
- 协程销毁函数:
void __fiboDestroy(__fiboFrame *__f);
2.3.1.1 协程帧定义
struct __fiboFrame {
void (*resume_fn)(__fiboFrame *); // 指向协程恢复函数 __fiboResume 的函数指针
void (*destroy_fn)(__fiboFrame *); // 指向协程销毁函数 __fiboDestroy 的函数指针
std::__coroutine_traits_impl<generator<unsigned int> >::promise_type __promise; // 协程的 promise 对象
int __suspend_index; // 记录协程当前挂起点/恢复点的索引
//bool __initial_await_suspend_called; // TODO: 将其修改为 __initial_await_resume_called,正确性待确认
bool __initial_await_resume_called; // 标记是否已完成 __promise.initial_suspend() 的执行
int n; // 协程函数参数
unsigned int a; // 协程局部变量(具有自动存储期的变量,实际为另外的保存方式)
unsigned int b; // 协程局部变量
int i; // 协程局部变量
unsigned int next; // 协程临时变量
std::suspend_never __suspend_81_21; // __promise.initial_suspend() 返回的 awaiter
std::suspend_always __suspend_84_5; // __promise.yield_value() 返回的 awaiter
std::suspend_always __suspend_81_21_1; // __promise.final_suspend() 返回的 awaiter
};
2.3.1.2 协程函数
/* 创建一个协程帧对象 __f,初始化其成员变量,
然后调用协程的恢复函数 __fiboResume(),并在协程第一次挂起时,返回协程的返回对象 */
generator<unsigned int> fibo(int n) {
/* Allocate the frame including the promise. 分配包含 promise 的协程帧内存空间 */
/* Note: The actual parameter new is __builtin_coro_size */
__fiboFrame *__f = reinterpret_cast<__fiboFrame *>(operator new(sizeof(__fiboFrame)));
__f->__suspend_index = 0;
__f->__initial_await_resume_called = false;
__f->n = std::forward<int>(n); // 按值传递的形参被移动或复制,按引用传递的形参保持为引用
/* Construct the promise. 调用 promise 对象的构造函数 */
new (&__f->__promise) std::__coroutine_traits_impl<generator<unsigned int> >::promise_type{};
/* Forward declare the resume and destroy function. 前向声明 */
void __fiboResume(__fiboFrame * __f);
void __fiboDestroy(__fiboFrame * __f);
/* Assign the resume and destroy function pointers. 赋值函数指针 */
__f->resume_fn = &__fiboResume;
__f->destroy_fn = &__fiboDestroy;
/* Call the made up function with the coroutine body for initial suspend.
This function will be called subsequently by coroutine_handle<>::resume()
which calls __builtin_coro_resume(__handle_)
调用包含协程函数体的 __fiboResume() 函数,执行 __promise.initial_suspend();
后续在执行 coroutine_handle<>::resume() 时,也会调用 __fiboResume() 函数 */
__fiboResume(__f);
return __f->__promise.get_return_object(); // 获取协程返回对象,并将其返回
}
2.3.1.3 协程恢复函数
/* This function invoked by coroutine_handle<>::resume()
根据协程帧对象的 __suspend_index 值执行协程的挂起和恢复操作,
包括执行协程的初始挂起和恢复、循环中的挂起和恢复,以及结束挂起;
同时,还处理了协程的返回,和执行过程中可能出现的异常情况。
*/
void __fiboResume(__fiboFrame *__f) // 未对 co_await 进行转换
{
try
{
/* Create a switch to get to the correct resume point
根据协程帧对象 __f 的 __suspend_index 值跳转到正确的恢复点 */
switch (__f->__suspend_index) {
case 0: // 执行 __promise.initial_suspend(); 可能会挂起
break;
case 1: // 恢复 __promise.initial_suspend();
goto __resume_fibo_1; // 执行协程函数体,包括执行 __promise.yield_value() 后挂起
case 2:
goto __resume_fibo_2; // 恢复 __promise.yield_value(),继续进行协程函数体的循环处理
}
co_await __f->__promise.initial_suspend(); // 执行协程的初始挂起操作
// 设置 __f->__suspend_index = 1;
// 设置 __f->__initial_await_resume_called = true;
__resume_fibo_1:
// 协程函数体开始
__f->a = 0;
__f->b = 1;
for (__f->i = 0; __f->i < __f->n; __f->i++) {
co_await __f->__promise.yield_value<unsigned int &>(__f->b); // 转换 co_yield __f->b;
// 设置 __f->__suspend_index = 2;
__resume_fibo_2:
__f->next = (__f->a + __f->b);
__f->a = __f->b;
__f->b = __f->next;
}
__f->__promise.return_void(); // 转换 co_return;
goto __final_suspend; // 当协程通过 co_return 返回时,会转到 __final_suspend
// 协程函数体结束
}
catch (...)
{
if (!__f->__initial_await_resume_called) { // 如果在协程函数体开始前抛出异常,
throw; // 会销毁协程,释放协程帧内存,并将异常抛回给它的调用者?
}
__f->__promise.unhandled_exception(); // 如果在协程函数体内抛出异常,通过 __promise 进行异常处理
}
__final_suspend:
co_await __f->__promise.final_suspend(); // 执行协程的结束挂起操作
; // 结束协程执行
}
关于 co_await
部分的代码转换,会在 3 co_await 表达式 部分,进行展开说明。
2.3.1.4 协程销毁函数
/* This function invoked by coroutine_handle<>::destroy()
销毁协程帧对象 __f,包括其中的变量,并释放内存空间 */
void __fiboDestroy(__fiboFrame *__f) {
/* destroy all variables with dtors. 销毁所有具有析构函数的变量,
包括:Promise 对象、协程实参、协程局部变量等 */
__f->~__fiboFrame();
/* Deallocating the coroutine frame. 释放协程帧对象的内存空间 */
/* Note: The actual argument to delete is __builtin_coro_frame with the promise as parameter */
operator delete(static_cast<void *>(__f));
}
2.3.2 执行时序
参考编译器对协程代码的转换情况,整理生成器的执行时序如下:
2.4 补充示例
本节通过示例 fibo2()
完善协程函数,扩展多处执行 co_yield
和 co_return
的情况,以及异常处理情况。
// 协程函数,生成斐波那契数列 0,1,1,2,3,5,8...
generator<unsigned> fibo2(int n) {
if (n == 0) co_return; // 直接结束协程
co_yield 0; // 将 0 缓存到 promise 后,挂起
if (n == 1) co_return; // 恢复执行,如果 n 为 1,结束协程
co_yield 1; // 将 1 缓存到 promise 后,挂起
if (n == 2) co_return; // 恢复执行,如果 n 为 2,结束协程
unsigned a = 0, b = 1;
for (int i = 2; i < n; i++) {
auto s = a + b;
if (s < b) { // 元素溢出,抛出异常,触发 promise_type::unhandled_exception()
throw std::runtime_error(std::string("fib(") + std::to_string(i) + std::string(") overflowed"));
}
co_yield s; // 将 s 缓存到 promise 后,挂起
a = b; // 恢复执行,更新变量后进行下一轮计算,或结束协程
b = s;
}
}
int main() {
try {
auto f = fibo2(100); // 测试异常情况,最大值应为 48,避免 unsigned(uint32_t) 溢出
for (int i = 0; !f.done(); f.resume(), i++) {
std::cout << "fib(" << i << ") = " << f.value() << '\n';
}
} catch (const std::exception& ex) {
std::cout << "Exception: " << ex.what() << '\n';
} catch (...) {
std::cout << "Unknown exception.\n";
}
}
3 co_await 表达式
3.1 co_await 分析
await
也被称为 asynchronous wait,即异步等待,在等待异步执行结果的过程中,让出当前线程的控制权。
在语句 auto result = co_await expr;
中,主要包含三部分:
- 一元运算符
co_await
,可以被重载; - 操作数
expr
,是一个表达式,可能会经过多步转换,最终为awaiter
类型; - 返回结果
result
,是awaiter::await_resume()
的返回值。
3.1.1 表达式转换
编译器会对 co_await expr
表达式中的 expr
进行两步转换,以便对表达式的语义进行定制,并得到最终的 awaiter
对象。
- 转换得到
awaitable
对象:- 检查协程
Promise
对象是否存在成员函数await_transform
, - 如果存在,则令
awaitable
对象为await_transform(expr)
调用后的结果; - 如果不存在,则令
awaitable
对象为expr
。
- 检查协程
- 转换得到
awaiter
对象:- 检查
awaitable
对象是否重载了operator co_await()
运算符, - 如果有重载,则令
awaiter
对象为重载函数调用后的结果; - 如果无重载,则令
awaiter
对象为awaitable
对象(可能就是最初的expr
)。
- 检查
3.1.2 awaiter 对象
awaiter
类型需要程序提供如下三个接口的实现:
序号 | 接口 | 是否必需 | 说明 |
---|---|---|---|
1 | bool await_ready(); | 是 | 判断当前协程是否需要在此处挂起,如果挂起,会执行 await_suspend ;否则(同步操作或结果已就绪),执行 await_resume 获得结果。 |
2 | void await_suspend(coroutine_handle<>); bool await_suspend(coroutine_handle<>); coroutine_handle<> await_suspend(coroutine_handle<>); | 三选一 | 通常程序可以在此处发起一个异步操作,然后返回协程的调用者或恢复者;在完成异步操作后,对当前协程句柄执行 resume 即可恢复协程的执行。1)返回 void 类型,协程保持挂起;2)返回 bool 类型,如果值为 true ,则协程保持挂起,否则,直接恢复当前协程,即,继续执行 await_resume 及其后续代码;3)返回 coroutine_handle 类型,协程保持挂起,同时恢复它所返回的协程句柄(可能导致当前协程恢复)。如果 await_suspend 抛出异常,协程也会恢复。 |
3 | auto await_resume(); | 是 | 在协程恢复时执行,其返回值将作为整个 co_await 表达式的值。 |
awaiter
类型能够让程序定制 co_await
表达式的语义,控制当前协程是否挂起,挂起后需要执行哪些逻辑供将来恢复,以及恢复后如何产生该表达式的值。
通过定义不同的 awaiter
,就可以对各种异步操作进行协程化实现。
3.1.3 标准库 awaiter
C++ 标准库中提供了两个最基本的 awaiter
,分别为:
suspend_always
,通过await_ready()
接口返回false
,挂起当前协程;suspend_never
,通过await_ready()
接口返回true
,不挂起当前协程。
// 头文件:include/c++/10.5.0/coroutine
struct suspend_always {
constexpr bool await_ready() const noexcept { return false; }
constexpr void await_suspend(coroutine_handle<>) const noexcept {}
constexpr void await_resume() const noexcept {}
};
struct suspend_never {
constexpr bool await_ready() const noexcept { return true; }
constexpr void await_suspend(coroutine_handle<>) const noexcept {}
constexpr void await_resume() const noexcept {}
};
3.1.4 co_await 处理流程
co_await
表达式的处理流程示意:
3.1.5 fibo 的 co_await 转换
在示例代码 fibo
中,使用的都是上述标准库中的 awaiter
。对 __fiboResume()
的 co_await
进行转换后,代码如下:
void __fiboResume(__fiboFrame *__f) // 对 co_await 进行转换
{
try
{
switch (__f->__suspend_index) {
case 0: break;
case 1: goto __resume_fibo_1;
case 2: goto __resume_fibo_2;
}
/* co_await insights.cpp:81
对 co_await __f->__promise.initial_suspend(); 进行转换 */
__f->__suspend_81_21 = __f->__promise.initial_suspend(); // 获取 awaiter,赋值给 __suspend_81_21
if (!__f->__suspend_81_21.await_ready()) { // 如果没有 ready,则挂起,然后执行 await_suspend() 并返回
__f->__suspend_81_21.await_suspend(
std::coroutine_handle<generator<unsigned int>::promise_type>::from_address(static_cast<void *>(__f))
.operator std::coroutine_handle<void>()); // TODO: 不应该执行 resume()?
__f->__suspend_index = 1; // 更新下次执行时的恢复点索引
//__f->__initial_await_suspend_called = true; // TODO: 实现方式待确认
return;
}
__resume_fibo_1:
__f->__suspend_81_21.await_resume(); // __promise.initial_suspend() 恢复执行,在下次挂起/协程结束时返回调用者/恢复者
__f->__initial_await_resume_called = true;
__f->a = 0;
__f->b = 1;
for (__f->i = 0; __f->i < __f->n; __f->i++) {
/* co_yield insights.cpp:84
对 co_await __f->__promise.yield_value<unsigned int &>(__f->b); 进行转换 */
__f->__suspend_84_5 = __f->__promise.yield_value<unsigned int &>(__f->b); // 每次循环,都会创建新的 awaiter
if (!__f->__suspend_84_5.await_ready()) {
__f->__suspend_84_5.await_suspend(
std::coroutine_handle<generator<unsigned int>::promise_type>::from_address(static_cast<void *>(__f))
.operator std::coroutine_handle<void>()); // TODO: 同 __suspend_81_21.await_suspend()
__f->__suspend_index = 2;
return;
}
__resume_fibo_2:
__f->__suspend_84_5.await_resume();
__f->next = (__f->a + __f->b);
__f->a = __f->b;
__f->b = __f->next;
}
/* co_return insights.cpp:89 */
__f->__promise.return_void();
goto __final_suspend;
}
catch (...)
{
if (!__f->__initial_await_resume_called) {
throw;
}
__f->__promise.unhandled_exception();
}
__final_suspend:
/* co_await insights.cpp:81
对 co_await __f->__promise.final_suspend(); 进行转换 */
__f->__suspend_81_21_1 = __f->__promise.final_suspend();
if (!__f->__suspend_81_21_1.await_ready()) { // 如果在这里挂起,后续只能通过协程句柄手动销毁,并且不能再次恢复协程,
__f->__suspend_81_21_1.await_suspend( // 并且协程句柄的 done() 接口将返回 true
std::coroutine_handle<generator<unsigned int>::promise_type>::from_address(static_cast<void *>(__f))
.operator std::coroutine_handle<void>()); // TODO: 同 __suspend_81_21.await_suspend()
// TODO: 需要 return; ?
}
; // TODO: 转换代码未完成?
// 可能需要调用 __fiboDestroy(__f); 即,如果 __promise.final_suspend() 没有挂起,将自动销毁协程
}
3.2 示例代码
通过 co_await
执行异步任务:
// 编译指令:g++ -fcoroutines -std=c++20 -lpthread demo_task.cc
#include <coroutine>
#include <iostream>
#include <stdexcept>
#include <thread>
struct task {
struct promise_type {
int value_;
task get_return_object() { return {std::coroutine_handle<promise_type>::from_promise(*this)}; }
auto initial_suspend() { return std::suspend_never{}; }
auto final_suspend() noexcept { return std::suspend_always{}; }
void return_value(int value) { value_ = value; }
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle<promise_type> h_;
task(std::coroutine_handle<promise_type> h) : h_(h) {}
~task() { h_.destroy(); }
};
auto task_proc(int in_data, std::jthread& new_thd) {
struct awaiter {
int data_;
std::jthread* pthd_;
bool await_ready() { return false; }
auto await_resume() { return data_; }
void await_suspend(std::coroutine_handle<> h) {
std::jthread& thd = *pthd_;
if (thd.joinable()) throw std::runtime_error("Output jthread parameter not empty");
thd = std::jthread([h, this] {
std::this_thread::sleep_for(std::chrono::seconds(3)); // 模拟耗时处理
data_ += 1000;
h.resume();
});
// 不能使用 pthd_->get_id(),因为在新线程中执行 h.resume() 后,*this 会被销毁
std::cout << "New thread ID: " << thd.get_id() << '\n';
}
};
return awaiter{in_data, &new_thd};
}
task coro_task(int in_data, std::jthread& new_thd) {
std::cout << "Coroutine started on thread: " << std::this_thread::get_id() << '\n';
auto result = co_await task_proc(in_data, new_thd);
// awaiter destroyed here
std::cout << "Coroutine resumed on thread: " << std::this_thread::get_id() << '\n';
co_return result;
}
int main() {
std::jthread new_thd;
int in_data = 10;
auto t = coro_task(in_data, new_thd);
while (!t.h_.done()) {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "waiting..." << '\n';
}
std::cout << "result: " << t.h_.promise().value_ << '\n';
}
3.2.1 编译转换代码
以下是通过 cppinsights.io 获得的,未做修改和注释的,转换后的协程代码:
在语句 auto result = co_await task_proc(in_data, new_thd);
中,awaiter __suspend_45_26
的返回值被保存在了 __suspend_45_26_res
中。
/*************************************************************************************
* NOTE: The coroutine transformation you've enabled is a hand coded transformation! *
* Most of it is _not_ present in the AST. What you see is an approximation. *
*************************************************************************************/
struct __coro_taskFrame
{
void (*resume_fn)(__coro_taskFrame *);
void (*destroy_fn)(__coro_taskFrame *);
std::__coroutine_traits_impl<task>::promise_type __promise;
int __suspend_index;
bool __initial_await_suspend_called;
int in_data;
std::jthread & new_thd;
int result;
std::suspend_never __suspend_43_6;
awaiter __suspend_45_26;
int __suspend_45_26_res;
std::suspend_always __suspend_43_6_1;
};
task coro_task(int in_data, std::jthread & new_thd)
{
/* Allocate the frame including the promise */
/* Note: The actual parameter new is __builtin_coro_size */
__coro_taskFrame * __f = reinterpret_cast<__coro_taskFrame *>(operator new(sizeof(__coro_taskFrame)));
__f->__suspend_index = 0;
__f->__initial_await_suspend_called = false;
__f->in_data = std::forward<int>(in_data);
__f->new_thd = std::forward<std::jthread &>(new_thd);
/* Construct the promise. */
new (&__f->__promise)std::__coroutine_traits_impl<task>::promise_type{};
/* Forward declare the resume and destroy function. */
void __coro_taskResume(__coro_taskFrame * __f);
void __coro_taskDestroy(__coro_taskFrame * __f);
/* Assign the resume and destroy function pointers. */
__f->resume_fn = &__coro_taskResume;
__f->destroy_fn = &__coro_taskDestroy;
/* Call the made up function with the coroutine body for initial suspend.
This function will be called subsequently by coroutine_handle<>::resume()
which calls __builtin_coro_resume(__handle_) */
__coro_taskResume(__f);
return __f->__promise.get_return_object();
}
/* This function invoked by coroutine_handle<>::resume() */
void __coro_taskResume(__coro_taskFrame * __f)
{
try
{
/* Create a switch to get to the correct resume point */
switch(__f->__suspend_index) {
case 0: break;
case 1: goto __resume_coro_task_1;
case 2: goto __resume_coro_task_2;
}
/* co_await insights.cpp:43 */
__f->__suspend_43_6 = __f->__promise.initial_suspend();
if(!__f->__suspend_43_6.await_ready()) {
__f->__suspend_43_6.await_suspend(std::coroutine_handle<task::promise_type>::from_address(static_cast<void *>(__f)).operator std::coroutine_handle<void>());
__f->__suspend_index = 1;
__f->__initial_await_suspend_called = true;
return;
}
__resume_coro_task_1:
__f->__suspend_43_6.await_resume();
std::operator<<(std::operator<<(std::operator<<(std::cout, "Coroutine started on thread: "), std::this_thread::get_id()), '\n');
/* co_await insights.cpp:45 */
__f->__suspend_45_26 = task_proc(__f->in_data, __f->new_thd);
if(!__f->__suspend_45_26.await_ready()) {
__f->__suspend_45_26.await_suspend(std::coroutine_handle<task::promise_type>::from_address(static_cast<void *>(__f)).operator std::coroutine_handle<void>());
__f->__suspend_index = 2;
return;
}
__resume_coro_task_2:
__f->__suspend_45_26_res = __f->__suspend_45_26.await_resume();
__f->result = __f->__suspend_45_26_res;
std::operator<<(std::operator<<(std::operator<<(std::cout, "Coroutine resumed on thread: "), std::this_thread::get_id()), '\n');
/* co_return insights.cpp:48 */
__f->__promise.return_value(__f->result);
goto __final_suspend;
}
catch(...)
{
if(!__f->__initial_await_suspend_called) {
throw ;
}
__f->__promise.unhandled_exception();
}
__final_suspend:
/* co_await insights.cpp:43 */
__f->__suspend_43_6_1 = __f->__promise.final_suspend();
if(!__f->__suspend_43_6_1.await_ready()) {
__f->__suspend_43_6_1.await_suspend(std::coroutine_handle<task::promise_type>::from_address(static_cast<void *>(__f)).operator std::coroutine_handle<void>());
}
;
}
/* This function invoked by coroutine_handle<>::destroy() */
void __coro_taskDestroy(__coro_taskFrame * __f)
{
/* destroy all variables with dtors */
__f->~__coro_taskFrame();
/* Deallocating the coroutine frame */
/* Note: The actual argument to delete is __builtin_coro_frame with the promise as parameter */
operator delete(static_cast<void *>(__f));
}
参考
- 罗能著.C++20高级编程.机械工业出版社.2022.
- cppreference:Coroutines (C++20)
- David Mazières: My tutorial and take on C++20 coroutines February, 2021
宁静以致远,感谢 King 老师。