Coroutines (C++20)
协程是一种能够暂停执行并在之后恢复的函数。协程是无栈的:它们通过返回到调用者来暂停执行,且恢复执行所需的数据与栈分开存储。这使得顺序代码能够异步执行(例如,无需显式回调即可处理非阻塞I/O),同时支持对惰性计算的无限序列进行算法操作及其他用途。
满足以下任一条件的函数即为协程:
- co_await 表达式 —— 用于暂停执行直至恢复
task<> tcp_echo_server() { char data[1024]; while (true) { std::size_t n = co_await socket.async_read_some(buffer(data)); co_await async_write(socket, buffer(data, n)); } }
- the co_yield 表达式 —— 用于暂停执行并返回一个值
generator<unsigned int> iota(unsigned int n = 0) { while (true) co_yield n++; }
- co_return 语句 —— 用于完成执行并返回值
lazy<int> f() { co_return 7; }
每个协程都必须具有满足以下若干要求的返回类型。
目录 |
限制条件
协程不能使用
可变参数
、普通
返回
语句或
占位符返回类型
(
auto
或
概念
)。
Consteval 函数 、 constexpr 函数 、 构造函数 、 析构函数 以及 main 函数 不能作为协程使用。
执行
每个协程关联于
- promise 对象 ,在协程内部进行操作。协程通过此对象提交其结果或异常。promise 对象与 std::promise 完全无关。
- coroutine handle ,在协程外部进行操作。这是一个非拥有式句柄,用于恢复协程执行或销毁协程帧。
- coroutine state ,是内部的动态分配存储(除非分配被优化掉),包含以下内容的对象
-
- promise 对象
- 参数(全部按值复制)
- 当前挂起点的某种表示形式,以便 resume 操作知道从何处继续执行,destroy 操作知道哪些局部变量在作用域内
- 生命周期跨越当前挂起点的局部变量和临时变量
当协程开始执行时,它会执行以下操作:
- 使用 operator new 分配 协程状态对象。
- 将所有函数形参复制到协程状态:按值传递的形参将被移动或复制,按引用传递的形参保持引用关系(因此,如果在引用对象生命周期结束后恢复协程,可能导致悬空引用——详见后续示例)。
- 调用 promise 对象的构造函数。如果 promise 类型具有接收所有协程参数的构造函数,则使用复制后的协程实参调用该构造函数;否则调用默认构造函数。
- 调用 promise. get_return_object ( ) 并将结果保存在局部变量中。该调用的结果将在协程首次暂停时返回给调用方。在此步骤之前(含)抛出的任何异常都会传播回调用方,不会存入 promise 对象。
-
调用
promise.
initial_suspend
(
)
并对结果执行
co_await。典型的Promise类型要么返回 std::suspend_always (用于惰性启动的协程),要么返回 std::suspend_never (用于急切启动的协程)。 - 当 co_await promise. initial_suspend ( ) 恢复执行时,开始执行协程函数体。
参数变为悬垂的一些示例:
#include <coroutine> #include <iostream> struct promise; struct coroutine : std::coroutine_handle<promise> { using promise_type = ::promise; }; struct promise { coroutine get_return_object() { return {coroutine::from_promise(*this)}; } std::suspend_always initial_suspend() noexcept { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void return_void() {} void unhandled_exception() {} }; struct S { int i; coroutine f() { std::cout << i; co_return; } }; void bad1() { coroutine h = S{0}.f(); // S{0} 已被销毁 h.resume(); // 恢复的协程执行 std::cout << i,在释放后使用 S::i h.destroy(); } coroutine bad2() { S s{0}; return s.f(); // 返回的协程在恢复时无法避免释放后使用 } void bad3() { coroutine h = [i = 0]() -> coroutine // 同时也是协程的 lambda 表达式 { std::cout << i; co_return; }(); // 立即调用 // lambda 已销毁 h.resume(); // 在释放后使用(匿名 lambda 类型)::i h.destroy(); } void good() { coroutine h = [](int i) -> coroutine // 将 i 作为协程参数 { std::cout << i; co_return; }(0); // lambda 已销毁 h.resume(); // 没有问题,i 已作为按值参数复制到协程帧中 // frame as a by-value parameter h.destroy(); }
当协程到达挂起点
- 先前获取的返回对象将返回给调用者/恢复者,如有必要,会隐式转换为协程的返回类型。
当协程执行到 co_return 语句时,将执行以下操作:
- 对 promise. return_void ( ) 进行调用
-
- co_return ;
- co_return expr ; 其中 expr 具有 void 类型
- 对于 co_return expr ; (其中 expr 具有非 void 类型),调用 promise. return_value ( expr )
- 按创建顺序的逆序销毁所有具有自动存储期的变量
- 调用 promise. final_suspend ( ) 并对结果执行 co_await
从协程末尾退出等同于
co_return
;
,区别在于:如果在
Promise
作用域内找不到任何
return_void
声明,则其行为是未定义的。若函数体中不包含任何定义性关键字,无论其返回类型如何,该函数都不是协程;并且当返回类型不是(可能带有 cv 限定符的)
void
时,从末尾退出将导致未定义行为。
// 假设 task 是某种协程任务类型 task<void> f() { // 不是协程,未定义行为 } task<void> g() { co_return; // 正确 } task<void> h() { co_await g(); // 正确,隐式 co_return; }
如果协程以未捕获的异常结束,它将执行以下操作:
- 捕获异常并在 catch 块内调用 promise. unhandled_exception ( )
- 调用 promise. final_suspend ( ) 并对结果执行 co_await (例如恢复延续任务或发布结果)。从此处恢复协程将导致未定义行为。
当协程状态因通过 co_return 或未捕获异常而终止,或因通过其句柄被销毁而销毁时,它会执行以下操作:
- 调用 promise 对象的析构函数。
- 调用函数参数副本的析构函数。
- 调用 operator delete 释放协程状态所占用的内存。
- 将执行权交还给调用者/恢复者。
动态分配
协程状态通过非数组形式的 operator new 进行动态分配。
如果
Promise
类型定义了类级别的替换,则将使用该替换,否则将使用全局的
operator new
。
如果
Promise
类型定义了接受额外参数的
operator new
放置形式,且这些参数与参数列表匹配(其中第一个参数是请求的大小(类型为
std::size_t
),其余参数是协程函数参数),那么这些参数将被传递给
operator new
(这使得对协程使用
前导分配器约定
成为可能)。
对 operator new 的调用可以被优化掉(即使使用了自定义分配器),如果
- 协程状态的生命周期严格嵌套在调用者的生命周期内,且
- 协程帧的大小在调用点已知。
在这种情况下,协程状态会嵌入到调用者的栈帧中(如果调用者是普通函数)或协程状态中(如果调用者是协程)。
如果分配失败,协程将抛出
std::bad_alloc
,除非
Promise
类型定义了成员函数
Promise
::
get_return_object_on_allocation_failure
(
)
。若定义了该成员函数,分配将使用
operator new
的无异常抛出形式,并在分配失败时,协程立即将从
Promise
::
get_return_object_on_allocation_failure
(
)
获取的对象返回给调用方,例如:
struct Coroutine::promise_type { /* ... */ // 确保使用不抛出异常的 operator-new static Coroutine get_return_object_on_allocation_failure() { std::cerr << __func__ << '\n'; throw std::bad_alloc(); // 或者返回 Coroutine(nullptr); } // 自定义不抛出异常的 new 重载 void* operator new(std::size_t n) noexcept { if (void* mem = std::malloc(n)) return mem; return nullptr; // 分配失败 } };
Promise
Promise
类型由编译器通过
std::coroutine_traits
从协程的返回类型推导确定。
形式上,令
-
R和Args...分别表示协程的返回类型和参数类型列表, -
ClassT表示协程所属的类类型(若其定义为非静态成员函数), - cv 表示在 函数声明 中声明的 cv 限定符(若其定义为非静态成员函数),
其
Promise
类型由以下因素决定:
- std:: coroutine_traits < R, Args... > :: promise_type ,若协程未被定义为 隐式对象成员函数 ,
-
std::
coroutine_traits
<
R,
cvClassT & , Args... > :: promise_type ,若协程被定义为非右值引用限定的隐式对象成员函数, -
std::
coroutine_traits
<
R,
cvClassT && , Args... > :: promise_type ,若协程被定义为右值引用限定的隐式对象成员函数。
例如:
| 若协程定义为... |
则其
Promise
类型为...
|
|---|---|
| task < void > foo ( int x ) ; | std:: coroutine_traits < task < void > , int > :: promise_type |
| task < void > Bar :: foo ( int x ) const ; | std:: coroutine_traits < task < void > , const Bar & , int > :: promise_type |
| task < void > Bar :: foo ( int x ) && ; | std:: coroutine_traits < task < void > , Bar && , int > :: promise_type |
co_await
一元运算符 co_await 会暂停协程的执行并将控制权返还给调用者。
co_await
表达式
|
|||||||||
co_await 表达式仅可出现在常规 函数体 (包括 lambda 表达式 的函数体)内的 潜在求值表达式 中,且不得出现于
- 在 异常处理器 中,
- 在 声明语句 中,除非它出现在该声明语句的初始化器中,
-
在
初始化语句
的
简单声明
中(参见
if、switch、for和 [[../range- for |range- for ]]),除非它出现在该 初始化语句 的初始化器中 , - 在 默认参数 中,或
- 在具有静态或线程 存储期 的块作用域变量的初始化器中。
| (since C++26) |
首先, expr 按如下方式转换为可等待对象:
- 如果 expr 由初始暂停点、最终暂停点或 yield 表达式产生,则可等待对象保持 expr 原样。
-
否则,若当前协程的
Promise类型具有成员函数await_transform,则可等待对象为 promise. await_transform ( expr ) 。 - 否则,可等待对象保持 expr 原样。
随后,获取等待器对象,如下所示:
- 如果对 operator co_await 的重载解析给出了唯一最佳重载,则等待器是该调用的结果:
-
- awaitable. operator co_await ( ) 用于成员重载,
- operator co_await ( static_cast < Awaitable && > ( awaitable ) ) 用于非成员重载。
- 否则,若重载决议找不到运算符 co_await ,则等待器(awaiter)本身可作为可等待对象(awaitable)。
- 否则,若重载决议存在歧义,则程序非良构。
若上述表达式为 纯右值 ,则等待器对象是由其 实质化 生成的临时对象。否则,若上述表达式为 泛左值 ,则等待器对象即其所指代的对象。
然后,调用 awaiter. await_ready ( ) (这是一个快捷方式,用于在已知结果已就绪或可同步完成时避免挂起开销)。如果其结果在上下文转换为 bool 后为 false ,则
- 协程被暂停(其协程状态中填充了局部变量和当前暂停点)。
-
awaiter.
await_suspend
(
handle
)
被调用,其中 handle 是代表当前协程的协程句柄。在该函数内部,被暂停的协程状态可通过该句柄被观察,且该函数负责将其调度到某个执行器上恢复运行或安排其销毁(返回 false 视为调度操作)
-
若
await_suspend返回 void ,控制权立即返回给当前协程的调用者/恢复者(此协程保持暂停状态),否则 -
若
await_suspend返回 bool ,
-
- 值 true 将控制权返回给当前协程的调用者/恢复者
- 值 false 恢复当前协程
-
若
await_suspend返回其他协程的协程句柄,则该句柄被恢复(通过调用 handle. resume ( ) )(注意这可能链式触发最终导致当前协程恢复) -
若
await_suspend抛出异常,异常会被捕获,协程被立即恢复,并重新抛出该异常
-
若
最后,调用 awaiter. await_resume ( ) (无论协程是否被挂起),其返回值即为整个 co_await expr 表达式的结果。
如果协程在 co_await 表达式中被挂起,之后又被恢复执行,恢复点将位于调用 awaiter. await_resume ( ) 之前。
请注意,协程在进入 awaiter. await_suspend ( ) 之前已完全挂起。其句柄可被共享至其他线程,并在 await_suspend ( ) 函数返回前恢复执行。(请注意默认内存安全规则仍然适用,若协程句柄未经锁保护跨线程共享,则等待器应至少使用 释放语义 ,而恢复方应至少使用 获取语义 。)例如,可将协程句柄置入回调函数,在异步I/O操作完成时调度至线程池运行。在此场景下,由于当前协程可能已被恢复并执行了等待器对象的析构函数,这些操作与 await_suspend ( ) 在当前线程上继续执行的过程并发发生,因此 await_suspend ( ) 应将 * this 视为已销毁对象,在句柄发布至其他线程后不得再访问其成员。
示例
#include <coroutine> #include <iostream> #include <stdexcept> #include <thread> auto switch_to_new_thread(std::jthread& out) { struct awaitable { std::jthread* p_out; bool await_ready() { return false; } void await_suspend(std::coroutine_handle<> h) { std::jthread& out = *p_out; if (out.joinable()) throw std::runtime_error("Output jthread parameter not empty"); out = std::jthread([h] { h.resume(); }); // 潜在未定义行为:访问可能已被销毁的 *this // std::cout << "New thread ID: " << p_out->get_id() << '\n'; std::cout << "New thread ID: " << out.get_id() << '\n'; // 这样是安全的 } void await_resume() {} }; return awaitable{&out}; } struct task { struct promise_type { task get_return_object() { return {}; } std::suspend_never initial_suspend() { return {}; } std::suspend_never final_suspend() noexcept { return {}; } void return_void() {} void unhandled_exception() {} }; }; task resuming_on_new_thread(std::jthread& out) { std::cout << "Coroutine started on thread: " << std::this_thread::get_id() << '\n'; co_await switch_to_new_thread(out); // 等待器在此处被销毁 std::cout << "Coroutine resumed on thread: " << std::this_thread::get_id() << '\n'; } int main() { std::jthread out; resuming_on_new_thread(out); }
可能的输出:
Coroutine started on thread: 139972277602112 New thread ID: 139972267284224 Coroutine resumed on thread: 139972267284224
注意:等待器对象是协程状态的一部分(作为跨越挂起点的临时对象),它会在 co_await 表达式完成前被销毁。通过它可以维护某些异步I/O API所需的每次操作状态,而无需借助额外的动态内存分配。
标准库定义了两个基础可等待对象: std::suspend_always 与 std::suspend_never 。
|
本节内容不完整
原因:缺少示例 |
| promise_type :: await_transform 与程序提供的等待器演示 |
|---|
示例
运行此代码
#include <cassert> #include <coroutine> #include <iostream> struct tunable_coro { // 通过构造函数参数确定"就绪状态"的等待器 class tunable_awaiter { bool ready_; public: explicit(false) tunable_awaiter(bool ready) : ready_{ready} {} // 三个标准等待器接口函数: bool await_ready() const noexcept { return ready_; } static void await_suspend(std::coroutine_handle<>) noexcept {} static void await_resume() noexcept {} }; struct promise_type { using coro_handle = std::coroutine_handle<promise_type>; auto get_return_object() { return coro_handle::from_promise(*this); } static auto initial_suspend() { return std::suspend_always(); } static auto final_suspend() noexcept { return std::suspend_always(); } static void return_void() {} static void unhandled_exception() { std::terminate(); } // 用户提供的转换函数,返回自定义等待器: auto await_transform(std::suspend_always) { return tunable_awaiter(!ready_); } void disable_suspension() { ready_ = false; } private: bool ready_{true}; }; tunable_coro(promise_type::coro_handle h) : handle_(h) { assert(h); } // 为简化起见,将这4个特殊函数声明为已删除: tunable_coro(tunable_coro const&) = delete; tunable_coro(tunable_coro&&) = delete; tunable_coro& operator=(tunable_coro const&) = delete; tunable_coro& operator=(tunable_coro&&) = delete; ~tunable_coro() { if (handle_) handle_.destroy(); } void disable_suspension() const { if (handle_.done()) return; handle_.promise().disable_suspension(); handle_(); } bool operator()() { if (!handle_.done()) handle_(); return !handle_.done(); } private: promise_type::coro_handle handle_; }; tunable_coro generate(int n) { for (int i{}; i != n; ++i) { std::cout << i << ' '; // 传递给 co_await 的等待器会进入 promise_type::await_transform, // 该函数返回 tunable_awaiter,初始会导致挂起(每次迭代时返回到 main), // 但在调用 disable_suspension 后不再发生挂起, // 循环将运行至结束而不返回 main()。 co_await std::suspend_always{}; } } int main() { auto coro = generate(8); coro(); // 仅输出第一个元素 == 0 for (int k{}; k < 4; ++k) { coro(); // 输出 1 2 3 4,每次迭代一个 std::<span class="me |
co_yield
co_yield
表达式向调用者返回一个值并暂停当前协程:它是可恢复生成器函数的基础构建模块。
co_yield
表达式
|
|||||||||
co_yield
花括号初始化列表
|
|||||||||
它等价于
co_await promise.yield_value(expr)
典型的生成器
yield_value
会将其参数存储(复制/移动或仅存储地址,因为参数的生命周期跨越了
co_await
内部的暂停点)到生成器对象中,并返回
std::suspend_always
,从而将控制权转移给调用者/恢复者。
#include <coroutine> #include <cstdint> #include <exception> #include <iostream> template<typename T> struct Generator { // 类名'Generator'是我们的选择,协程不需要它 // 编译器通过'co_yield'关键字识别协程 // 只要包含嵌套结构体promise_type及其'MyGenerator get_return_object()'方法 // 你可以使用'MyGenerator'(或任何其他名称)代替 //(注意:重命名时需要调整构造函数和析构函数的声明) 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_always initial_suspend() { return {}; } std::suspend_always final_suspend() noexcept { return {}; } void unhandled_exception() { exception_ = std::current_exception(); } // 保存 // 异常 template<std::convertible_to<T> From> // C++20 概念 std::suspend_always yield_value(From&& from) { value_ = std::forward<From>(from); // 在promise中缓存结果 return {}; } void return_void() {} }; handle_type h_; Generator(handle_type h) : h_(h) {} ~Generator() { h_.destroy(); } explicit operator bool() { fill(); // 可靠地判断协程是否完成,以及是否会有下一个生成值(co_yield) // 的唯一方法是通过C++获取器(下面的operator())执行/恢复协程, // 直到下一个co_yield点(或让其自然结束)。 // 然后我们在promise中存储/缓存结果,允许获取器(下面的operator()) // 在不执行协程的情况下获取它。 return !h_.done(); } T operator()() { fill(); full_ = false; // 我们将移出先前缓存的结果, // 使promise再次为空 return std::move(h_.promise().value_); } private: bool full_ = false; void fill() { if (!full_) { h_(); if (h_.promise().exception_) std::rethrow_exception(h_.promise().exception_); // 在调用上下文中传播协程异常 full_ = true; } } }; Generator<std::uint64_t> fibonacci_sequence(unsigned n) { if (n == 0) co_return; if (n > 94) throw std::runtime_error("Too big Fibonacci sequence. Elements would overflow."); co_yield 0; if (n == 1) co_return; co_yield 1; if (n == 2) co_return; std::uint64_t a = 0; std::uint64_t b = 1; for (unsigned i = 2; i < n; ++i) { std::uint64_t s = a + b; co_yield s; a = b; b = s; } } int main() { try { auto gen = fibonacci_sequence(10); // uint64_t溢出前最大94 for (int j = 0; gen; ++j) std::cout << "fib(" << j << ")=" << gen() << '\n';
注释
| 功能测试 宏 | 值 | 标准 | 功能特性 |
|---|---|---|---|
__cpp_impl_coroutine
|
201902L
|
(C++20) | 协程 (编译器支持) |
__cpp_lib_coroutine
|
201902L
|
(C++20) | 协程 (库支持) |
__cpp_lib_generator
|
202207L
|
(C++23) | std::generator : 用于范围的同步协程生成器 |
关键词
co_await , co_return , co_yield
库支持
协程支持库 定义了若干类型,为协程提供编译时和运行时支持。
缺陷报告
以下行为变更缺陷报告被追溯应用于先前发布的C++标准。
| 缺陷报告 | 应用于 | 发布时的行为 | 正确行为 |
|---|---|---|---|
| CWG 2556 | C++20 |
无效的
return_void
导致从协程末尾退出的行为未定义
|
此情况下程序格式错误 |
| CWG 2668 | C++20 | co_await 不能出现在 lambda 表达式中 | 允许使用 |
| CWG 2754 | C++23 | 为显式对象成员函数构造 promise 对象时获取了 * this | 此情况下不获取 * this |
参见
|
(C++23)
|
表示同步
协程
生成器的
view
(类模板) |
外部链接
| 1. | Lewis Baker, 2017-2022 - 非对称传输。 |
| 2. | David Mazières, 2021 - C++20协程教程。 |
| 3. | 徐传奇 & 戚煜 & 韩垚, 2021 - C++20协程原理与应用。(中文) |
| 4. | Simon Tatham, 2023 - 编写自定义C++20协程系统。 |