协程 (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)); } }
- 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} destroyed h.resume(); // resumed coroutine executes std::cout << i, uses S::i after free h.destroy(); } coroutine bad2() { S s{0}; return s.f(); // returned coroutine can't be resumed without committing use after free } void bad3() { coroutine h = [i = 0]() -> coroutine // a lambda that's also a coroutine { std::cout << i; co_return; }(); // immediately invoked // lambda destroyed h.resume(); // uses (anonymous lambda type)::i after free h.destroy(); } void good() { coroutine h = [](int i) -> coroutine // make i a coroutine parameter { std::cout << i; co_return; }(0); // lambda destroyed h.resume(); // no problem, i has been copied to the coroutine // frame as a by-value parameter h.destroy(); }
当协程到达挂起点时
- 之前获得的返回值对象将返回给调用者/恢复者,如果需要,在隐式转换为协程的返回类型之后。
当协程到达 co_return 语句时,它会执行以下操作
- 对于
- co_return;
- co_return expr; 其中 expr 类型为 void
- 调用 promise.return_void() 或调用 promise.return_value(expr) 对于 co_return expr; 其中 expr 类型为非空类型
- 按创建顺序反向销毁所有具有自动存储期的变量。
- 调用 promise.final_suspend() 并 co_await 结果。
从协程末尾掉落等效于 co_return;,除了如果在 Promise
的作用域中找不到 return_void
的任何声明,则行为未定义。一个函数在其函数体中没有任何定义关键字不是协程,无论其返回类型如何,如果返回类型不是(可能是 cv 限定的)void,则从末尾掉落会导致未定义的行为。
// assuming that task is some coroutine task type task<void> f() { // not a coroutine, undefined behavior } task<void> g() { co_return; // OK } task<void> h() { co_await g(); // OK, implicit co_return; }
如果协程以未捕获的异常结束,它将执行以下操作
- 捕获异常并在 catch 块中调用 promise.unhandled_exception()
- 调用 promise.final_suspend() 并 co_await 结果(例如,恢复延续或发布结果)。从此时恢复协程是未定义的行为。
当协程状态被销毁,无论是因为它通过 co_return 或未捕获的异常而终止,还是因为它通过其句柄被销毁,它会执行以下操作
- 调用 promise 对象的析构函数。
- 调用函数参数副本的析构函数。
- 调用 operator delete 释放协程状态使用的内存。
- 将执行转移回调用者/恢复者。
[edit] 动态分配
协程状态通过非数组 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 的 nothrow 形式,在分配失败时,协程立即将从 Promise::get_return_object_on_allocation_failure() 获得的对象返回给调用者,例如
struct Coroutine::promise_type { /* ... */ // ensure the use of non-throwing operator-new static Coroutine get_return_object_on_allocation_failure() { std::cerr << __func__ << '\n'; throw std::bad_alloc(); // or, return Coroutine(nullptr); } // custom non-throwing overload of new void* operator new(std::size_t n) noexcept { if (void* mem = std::malloc(n)) return mem; return nullptr; // allocation failure } };
[edit] Promise
Promise
类型由编译器使用 std::coroutine_traits 从协程的返回类型确定。
形式上,令
-
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 |
[edit] co_await
一元运算符 co_await 暂停协程并将控制权返回给调用者。其操作数是表达式,该表达式要么
- 是定义了成员 operator co_await 的类类型,或者可以传递给非成员 operator co_await,或者
- 通过当前协程的 Promise::await_transform 可转换为这种类类型。
co_await expr |
|||||||||
co_await 表达式只能出现在常规 函数体(包括 lambda 表达式 的函数体)中的 可能被求值的 表达式中,并且不能出现在
- 处理程序 中,
- 声明 语句中,除非它出现在该声明语句的初始化程序中,
- init-statement (见
if
,switch
,for
和 range-for)的 简单声明 中,除非它出现在该 init-statement 的初始化程序中, - 默认参数 中,或者
- 具有静态或线程 存储期 的块作用域变量的初始化程序中。
首先,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)) 用于非成员重载。
- 否则,如果重载解析找不到 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() 函数返回之前恢复。(请注意,默认的内存安全规则仍然适用,因此如果协程句柄在没有锁的情况下跨线程共享,则等待器应至少使用 释放语义,而恢复者应至少使用 获取语义。)例如,协程句柄可以放在回调内部,在异步 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(); }); // Potential undefined behavior: accessing potentially destroyed *this // std::cout << "New thread ID: " << p_out->get_id() << '\n'; std::cout << "New thread ID: " << out.get_id() << '\n'; // this is OK } 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); // awaiter destroyed here 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 { // An awaiter whose "readiness" is determined via constructor's parameter. class tunable_awaiter { bool ready_; public: explicit(false) tunable_awaiter(bool ready) : ready_{ready} {} // Three standard awaiter interface functions: 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(); } // A user provided transforming function which returns the custom awaiter: 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); } // For simplicity, declare these 4 special functions as deleted: 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 << ' '; // The awaiter passed to co_await goes to promise_type::await_transform which // issues tunable_awaiter that initially causes suspension (returning back to // main at each iteration), but after a call to disable_suspension no suspension // happens and the loop runs to its end without returning to main(). co_await std::suspend_always{}; } } int main() { auto coro = generate(8); coro(); // emits only one first element == 0 for (int k{}; k < 4; ++k) { coro(); // emits 1 2 3 4, one per each iteration std::cout << ": "; } coro.disable_suspension(); coro(); // emits the tail numbers 5 6 7 all at ones } 输出 0 1 : 2 : 3 : 4 : 5 6 7 |
[编辑] co_yield
co_yield
表达式将值返回给调用者并挂起当前协程:它是可恢复生成器函数的常见构建块。
co_yield expr |
|||||||||
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 { // The class name 'Generator' is our choice and it is not required for coroutine // magic. Compiler recognizes coroutine by the presence of 'co_yield' keyword. // You can use name 'MyGenerator' (or any other name) instead as long as you include // nested struct promise_type with 'MyGenerator get_return_object()' method. // (Note: It is necessary to adjust the declarations of constructors and destructors // when renaming.) struct promise_type; using handle_type = std::coroutine_handle<promise_type>; struct promise_type // required { 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(); } // saving // exception template<std::convertible_to<T> From> // C++20 concept std::suspend_always yield_value(From&& from) { value_ = std::forward<From>(from); // caching the result in promise return {}; } void return_void() {} }; handle_type h_; Generator(handle_type h) : h_(h) {} ~Generator() { h_.destroy(); } explicit operator bool() { fill(); // The only way to reliably find out whether or not we finished coroutine, // whether or not there is going to be a next value generated (co_yield) // in coroutine via C++ getter (operator () below) is to execute/resume // coroutine until the next co_yield point (or let it fall off end). // Then we store/cache result in promise to allow getter (operator() below // to grab it without executing coroutine). return !h_.done(); } T operator()() { fill(); full_ = false; // we are going to move out previously cached // result to make promise empty again 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_); // propagate coroutine exception in called context 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); // max 94 before uint64_t overflows for (int j = 0; gen; ++j) std::cout << "fib(" << j << ")=" << gen() << '\n'; } catch (const std::exception& ex) { std::cerr << "Exception: " << ex.what() << '\n'; } catch (...) { std::cerr << "Unknown exception.\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
[编辑] 注释
特性测试 宏 | 值 | Std | 特性 |
---|---|---|---|
__cpp_impl_coroutine |
201902L | (C++20) | 协程(编译器支持) |
__cpp_lib_coroutine |
201902L | (C++20) | 协程(库支持) |
__cpp_lib_generator |
202207L | (C++23) | std::generator: 用于范围的同步协程生成器 |
[编辑] 关键字
[编辑] 库支持
协程支持库 定义了几个类型,用于提供协程的编译时和运行时支持。
[编辑] 缺陷报告
以下行为变更的缺陷报告被追溯地应用于以前发布的 C++ 标准。
DR | 应用于 | 发布的行为 | 正确行为 |
---|---|---|---|
CWG 2556 | C++20 | 无效的 return_void 使协程末尾的行为未定义 |
在这种情况下,程序是非法的 格式化 |
CWG 2668 | C++20 | co_await 不能出现在 lambda 表达式中 | 允许 |
CWG 2754 | C++23 | *this 在为显式对象成员函数 构造 promise 对象时被获取 |
*this 不会 在这种情况下被获取 |
[编辑] 另请参阅
(C++23) |
一个 视图 ,它表示同步 协程 生成器(类模板) |
[编辑] 外部链接
1. | Lewis Baker,2017-2022 - 非对称传输。 |
2. | David Mazières,2021 - C++20 协程教程。 |
3. | Chuanqi Xu & Yu Qi & Yao Han,2021 - C++20 协程原理及应用。(中文) |
4. | Simon Tatham,2023 - 编写自定义 C++20 协程系统。 |