协程 (C++20)

来自cppreference.com
< cpp‎ | language


 
 
 
 

协程是能暂停执行以在之后恢复的函数。协程是无栈的:它们通过返回到调用方暂停执行,并且恢复执行所需的数据与栈分离存储。这样就可以编写异步执行的顺序代码(例如不使用显式的回调来处理非阻塞输入/输出),还支持作用于惰性计算的无限序列上的算法及其他用途。

定义中包含了以下之一的函数是协程:

  • 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<unsignedint> iota(unsignedint 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-newstatic Coroutine get_return_object_on_allocation_failure(){std::cerr<< __func__ <<'\n';throwstd::bad_alloc();// 或者返回 Coroutine(nullptr);}   // 自定义重载不抛出 newvoid*operator new(std::size_t n)noexcept{if(void* mem =std::malloc(n))return mem;return nullptr;// 分配失败}};

[编辑]承诺

编译器用 std::coroutine_traits 从协程的返回类型确定承诺类型 Promise

正式而言,

  • RArgs... 分别代表协程的返回类型与形参类型列表,
  • 如果协程被定义为非静态成员函数,那么令 ClassT 代表协程所属的类,
  • 如果协程被定义为非静态成员函数,那么令 cv 代表协程的函数声明的 cv 限定,

以如下方式确定它的承诺类型 Promise

例如:

如果定义协程为那么它的承诺类型 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 表达式的函数体)里面的潜在求值表达式中出现,并且不能在以下位置出现:

co_await 表达式不可以是契约断言的谓词的潜在求值的子表达式。

(C++26 起)

首先,以下列方式将表达式 转换成可等待体:

  • 如果表达式 由初始暂停点、最终暂停点或 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(){returnfalse;}void await_suspend(std::coroutine_handle<> h){std::jthread& out =*p_out;if(out.joinable())throwstd::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_alwaysstd::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()constnoexcept{return ready_;}staticvoid await_suspend(std::coroutine_handle<>)noexcept{}staticvoid await_resume()noexcept{}};   struct promise_type {using coro_handle =std::coroutine_handle<promise_type>;auto get_return_object(){return coro_handle::from_promise(*this);}staticauto initial_suspend(){returnstd::suspend_always();}staticauto final_suspend()noexcept{returnstd::suspend_always();}staticvoid return_void(){}staticvoid 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();// 仅发出一个首元素 == 0for(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)throwstd::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(conststd::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_coroutine201902L(C++20)协程(编译器支持)
__cpp_lib_coroutine201902L(C++20)协程(库支持)
__cpp_lib_generator202207L(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 对显式对象成员函数构造承诺对象时会取 *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 协程系统
close