协程 (C++20)
协程是能暂停执行以在之后恢复的函数。协程是无栈的:它们通过返回到调用方暂停执行,并且恢复执行所需的数据与栈分离存储。这样就可以编写异步执行的顺序代码(例如不使用显式的回调来处理非阻塞输入/输出),还支持作用于惰性计算的无限序列上的算法及其他用途。
定义中包含了以下之一的函数是协程:
- 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)); } }
- co_yield 表达式——用于暂停执行并返回一个值:
generator<unsigned int> iota(unsigned int n = 0) { while (true) co_yield n++; }
- co_return 语句——用于完成执行并返回一个值:
lazy<int> f() { co_return 7; }
每个协程必须具有能够满足一组要求的返回类型,如下所述。
限制
协程不能使用变长实参,普通的 return 语句,或占位符返回类型(auto
或 概念)。
consteval 函数、constexpr 函数、构造函数、析构函数及 main
函数 不能是协程。
执行
每个协程都与下列对象关联:
- 承诺对象,在协程内部操纵。协程通过此对象提交其结果或异常。承诺对象和 std::promise 没有任何关系。
- 协程句柄,在协程外部操纵。这是用于恢复协程执行或销毁协程帧的不带所有权句柄。
- 协程状态,它是一个动态存储分配(除非优化掉其分配)的内部对象,其包含:
- 承诺对象
- 各个形参(全部按值复制)
- 当前暂停点的某种表示,使得程序在恢复时知晓要从何处继续,销毁时知晓有哪些局部变量在作用域内
- 生存期跨过当前暂停点的局部变量和临时量
当协程开始执行时,它进行下列操作:
- 用 operator new 分配协程状态对象。
- 将所有函数形参复制到协程状态中:按值传递的形参被移动或复制,按引用传递的形参保持为引用(因此,如果在被指代对象的生存期结束后恢复协程,它可能变成悬垂引用——参见下面的示例)。
- 调用承诺对象的构造函数。如果承诺类型拥有接收所有协程形参的构造函数,那么以复制后的协程实参调用该构造函数。否则调用其默认构造函数。
- 调用 promise.get_return_object() 并将结果保存在局部变量中。该调用的结果将在协程首次暂停时返回给调用方。至此并包含这个步骤为止,任何抛出的异常均传播回调用方,而非置于承诺中。
- 调用 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 已经作为按值传递的形参被复制到协程帧中 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; // OK } task<void> h() { co_await g(); // OK, 隐式 co_return; }
如果协程因未捕获的异常结束,那么它进行下列操作:
- 捕获异常并在处理块内调用 promise.unhandled_exception()。
- 调用 promise.final_suspend() 并 co_await 它的结果(例如,恢复某个继续或发布某个结果)。此时开始恢复协程是未定义行为。
当经由 co_return 或未捕获异常而终止协程导致协程状态被销毁,或通过它的句柄销毁它时,它进行下列操作:
- 调用承诺对象的析构函数。
- 调用各个函数形参副本的析构函数。
- 调用 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; // 分配失败 } };
承诺
编译器用 std::coroutine_traits 从协程的返回类型确定承诺类型 Promise
。
正式而言,
- 令
R
与Args...
分别代表协程的返回类型与形参类型列表, - 如果协程被定义为非静态成员函数,那么令
ClassT
代表协程所属的类, - 如果协程被定义为非静态成员函数,那么令 cv 代表协程的函数声明的 cv 限定,
以如下方式确定它的承诺类型 Promise
:
- std::coroutine_traits<R, Args...>::promise_type,如果协程未被定义为隐式对象成员函数,
- std::coroutine_traits<R,
- std::coroutine_traits<R,
例如:
如果定义协程为 | 那么它的承诺类型 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 暂停协程并将控制返回给调用方。它的操作数是一个表达式,它要么:
- 具有定义了成员 operator co_await 的类类型,或者可以被传递给某个非成员 operator co_await,要么
- 能以当前协程的 Promise::await_transform 转换到这种类类型。
co_await 表达式
|
|||||||||
co_await 表达式只能在常规函数体(包括 lambda 表达式的函数体)里面的潜在求值表达式中出现,并且不能在以下位置出现:
- 处理块中,
- 声明语句中,但可以在该声明语句的初始化式中出现,
- 初始化语句 的简单声明中(见
if
、switch
、for
以及范围 for),但可以在该初始化语句 的初始化式中出现, - 默认实参中
- 具有静态或线程存储期的块作用域变量的初始化式中。
首先,以下列方式将表达式 转换成可等待体:
- 如果表达式 由初始暂停点、最终暂停点或 yield 表达式所产生,那么可等待体是表达式 本身。
- 否则,如果当前协程的承诺类型
Promise
拥有成员函数await_transform
,那么可等待体是 promise.await_transform(表达式)。 - 否则,可等待体是表达式 本身。
然后以下列方式获得等待器对象:
- 如果针对 operator co_await 的重载决议给出单个最佳重载,那么等待器是该调用的结果:
- 对于成员重载为 awaitable.operator co_await();,
- 对于非成员重载为 operator co_await(static_cast<Awaitable&&>(awaitable));.
- 否则,如果重载决议找不到 operator co_await,那么等待器是可等待体本身。
- 否则,如果重载决议有歧义,那么程序非良构。
如果上述表达式为纯右值,那么等待器对象是从它实质化的临时量。否则,如果上述表达式为泛左值,那么等待器对象是它所指代的对象。
然后,调用 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() 函数返回前,可以将其句柄共享给另一线程并恢复执行。(要注意,默认的内存安全性规则仍适用,因此如果不加锁地跨线程共享协程句柄,那么其等待器至少应当使用释放语义,而且恢复方至少应当使用获得语义。)例如,可以将协程句柄放入回调内部,将它调度成在异步输入/输出操作完成时在线程池上运行等。此时因为当前协程可能已被恢复,从而执行了等待器对象的析构函数,同时由于 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("jthread 输出参数非空"); out = std::jthread([h] { h.resume(); }); // 潜在的未定义行为:访问潜在被销毁的 *this // std::cout << "新线程 ID:" << p_out->get_id() << '\n'; std::cout << "新线程 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 << "协程开始,线程 ID:" << std::this_thread::get_id() << '\n'; co_await switch_to_new_thread(out); // 等待器在此销毁 std::cout << "协程恢复,线程 ID:" << std::this_thread::get_id() << '\n'; } int main() { std::jthread out; resuming_on_new_thread(out); }
可能的输出:
协程开始,线程 ID:139972277602112 新线程 ID:139972267284224 协程恢复,线程 ID:139972267284224
注意:等待器对象是协程状态的一部分(作为生存期跨过暂停点的临时量),并且在 co_await 表达式结束前销毁。可以用它维护某些异步输入/输出 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); } // 为简化起见,将四个特殊函数声明为弃置: 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::cout << ": "; } coro.disable_suspension(); coro(); // 一次性发出剩余的数 5 6 7 } 输出: 0 1 : 2 : 3 : 4 : 5 6 7 |
co_yield
co_yield
表达式向调用方返回一个值并暂停当前协程:它是可恢复生成器函数的常用构建块。
co_yield 表达式
|
|||||||||
co_yield 花括号初始化式列表
|
|||||||||
它等价于
co_await promise.yield_value(表达式)
典型的生成器的 yield_value
会将其实参存储(复制/移动或仅存储它的地址,因为实参的生存期跨过 co_await
内的暂停点)到生成器对象中并返回 std::suspend_always,将控制转移给调用方/恢复方。
#include <coroutine> #include <cstdint> #include <exception> #include <iostream> template<typename T> struct Generator { // 类名 'Generator' 只是我们的选择,使用协程魔法不依赖它。 // 编译器通过关键词 'co_yield' 的存在识别协程。 // 你可以使用 'MyGenerator'(或者任何别的名字)作为替代,只要在类中包括了 // 拥有 'MyGenerator get_return_object()' 方法的嵌套 struct promise_type。 // (注意:在重命名时,你还需要调整构造函数/析构函数的声明。) 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); // 在承诺中缓存结果 return {}; } void return_void() {} }; handle_type h_; Generator(handle_type h) : h_(h) {} ~Generator() { h_.destroy(); } explicit operator bool() { fill(); // 获知协程是结束了还是仍能通过 C++ getter(下文的 operator()) // 获得下一个生成值的唯一可靠方式,是执行/恢复协程到下一个 co_yield 节点 // (或让执行流抵达结尾)。 // 我们在承诺中存储/缓存了执行结果,使得 getter(下文的 operator()) // 可以获得这一结果而不执行协程。 return !h_.done(); } T operator()() { fill(); full_ = false;// 我们将移动走先前缓存的结果来重新置空承诺 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<uint64_t> fibonacci_sequence(unsigned n) { if (n == 0) co_return; if (n > 94) throw std::runtime_error("斐波那契序列过大,元素将会溢出。"); co_yield 0; if (n == 1) co_return; co_yield 1; if (n == 2) co_return; uint64_t a = 0; uint64_t b = 1; for (unsigned i = 2; i < n; i++) { uint64_t s = a + b; co_yield s; a = b; b = s; } } int main() { try { auto gen = fibonacci_sequence(10); // 最大值94,避免 uint64_t 溢出 for (int j = 0; gen; j++) std::cout << "fib(" << j << ")=" << gen() << '\n'; } catch (const std::exception& ex) { std::cerr << "发生了异常:" << ex.what() << '\n'; } catch (...) { std::cerr << "未知异常。\n"; } }
输出:
fib(0)=0 fib(1)=1 fib(2)=1 fib(3)=2 fib(4)=3 fib(5)=5 fib(6)=8 fib(7)=13 fib(8)=21 fib(9)=34
注解
功能特性测试宏 | 值 | 标准 | 功能特性 |
---|---|---|---|
__cpp_impl_coroutine |
201902L | (C++20) | 协程(编译器支持) |
__cpp_lib_coroutine |
201902L | (C++20) | 协程(库支持) |
__cpp_lib_generator |
202207L | (C++23) | std::generator: 适用于范围的同步协程生成器 |
关键词
库支持
协程支持库定义数个类型,提供协程的编译与运行时支持。
缺陷报告
下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。
缺陷报告 | 应用于 | 出版时的行为 | 正确行为 |
---|---|---|---|
CWG 2556 | C++20 | 非法的 return_void 会导致控制流出协程的结尾的行为未定义
|
此时程序非良构 |
CWG 2668 | C++20 | co_await 不能在 lambda 表达式中出现 | 可以出现 |
CWG 2754 | C++23 | 对显式对象成员函数构造承诺对象时会取 *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 协程系统 |