跳转到内容

C++/coroutine

维基教科书,自由的教学读本
< C++

C++20引入了协程(coroutine)。协程是函数的一种泛化,它允许函数被挂起,然后在稍后被恢复执行。协程在“调用”和“返回”操作外,还有3种额外的操作:“挂起”、“恢复”、“摧毁”。[1]

  • 挂起(Suspend)操作是协程执行到co_awaitco_yield关键字处(称为挂起点),挂起自身的执行,把执行转交给协程的(最初的)调用者或者(中途的)恢复者。挂起操作首先准备堆上的协程帧(coroutine frame),包括写入是哪个挂起点以便将来作为恢复秩序的地址。如果要把执行转交给协程的调用者或者恢复者,则协程的栈帧被弹出运行站。
  • 恢复(Resume)操作:当一个函数要恢复一个已经被挂起的协程,它会调用协程帧(coroutine frame)中void resume()方法。resume()将分配协程的栈帧、把调用者的返回地址保存在栈帧中,然后转移到协程的上次挂起点继续执行。当协程再次被挂起或者执行完毕, resume()的调用者将恢复秩序。
  • 摧毁(Destroy)操作将摧毁一个挂起的协程。首先它类似于恢复操作将恢复协程的栈帧、保存摧毁操作调用者的返回地址。然后调用调用协程帧(coroutine frame)中void destroy()方法,调用所有作用域内的局部变量的析构函数,最后释放堆上的协程帧。
  • 调用(Call)操作:从调用者角度,调用一个协程和调用普通函数并无区别。但协程执行到第一个挂起点后,将恢复调用者的执行。协程被调用后,首先在堆上创建协程帧,从栈帧复制/移动参数到协程帧,这些参数的生命期需要跨越第一个挂起点。
  • 返回(Return)操作:与普通函数的返回略有不同。协程执行co_return时,在(可定制的)某处存储返回值,然后析构作用域内的局部变量(不含参数)。然后协程可执行一些可定制逻辑。最后协程挂起操作或者摧毁操作,把执行交给调用者或者恢复者。需要注意的是,传递给返回操作的返回值与从调用操作返回的返回值并不相同,因为返回操作可能在调用者从最初的调用操作恢复后很长时间才执行。

头文件<coroutine>提供了 3个新的C++关键字: co_await, co_yield and co_return 新的类型:

coroutine_handle

coroutine_traits<Ts...> suspend_always

Promise机制

[编辑]

可以认为协程是一个状态机。协程的promise对象可看作“协程状态控制器”对象,控制了协程的行为,可用于跟踪协程的状态。

协程执行到某一点时,在协程帧中创建协程的promise对象。编译器产生promise对象的若干方法。协程的执行大致为:

{
  co_await promise.initial_suspend();
  try
  {
    <body-statements>
  }
  catch (...)
  {
    promise.unhandled_exception();
  }
FinalSuspend:
  co_await promise.final_suspend();
}

调用协程时初始化要执行:

  1. 使用运算符new分配一个协程帧(可选)。
  2. 将任何函数参数复制到协程帧中。如果参数是通过值传递给协程的,那么这些参数将通过调用类型的移动构造函数复制到协程帧中。如果参数是通过引用(无论是左值还是右值)传递给协程的,那么只有引用被复制到协程帧中,而不是它们指向的值。在将参数通过引用传递到协程时,涉及许多陷阱,因为协程的生命周期内,引用不一定始终保持有效。许多通常用于普通函数的技术,如完美转发和通用引用,如果用于协程,可能会导致代码具有未定义行为。
  3. 调用类型为P的promise对象的构造函数。
  4. 调用promise.get_return_object()方法以获取当协程首次挂起时返回给调用者的结果。将结果保存为局部变量。
  5. 调用promise.initial_suspend()方法并使用co_await等待结果。这给了编程者一个机会在执行协程体之前先挂起执行。initial_suspend()方法要么返回std::suspend_always(如果操作是延迟启动的),要么返回std::suspend_never(如果操作是立即启动的),这两种都是 noexcept 的可等待对象
  6. 当co_await promise.initial_suspend()表达式恢复执行时(无论是立即还是异步),协程就开始执行你编写的协程主体语句

当执行到达一个 co_return 语句时(协程体执行完毕时,当作有一个co_return):

  1. 调用 promise.return_void() 或 promise.return_value(<expr>)。
  2. 以它们被创建的相反顺序销毁所有具有自动存储期限的变量。
  3. 调用 promise.final_suspend() 并使用 co_await 等待结果。

当执行由于未处理的异常而离开 <body-statements> 时:

  1. 捕获异常:在 catch 块内调用 promise.unhandled_exception()。典型是用std::current_exception()捕获异常。
  2. 调用 promise.final_suspend() 并使用 co_await 等待结果。因此有机会发布结果、发出完成信号或恢复继续执行。它还允许协程在执行运行完成并且协程帧被销毁之前选择性地立即挂起。请注意,对处于 final_suspend 点的协程调用 resume() 是未定义行为。你唯一能对挂起在这里的协程做的就是销毁它。

一旦执行离开了协程体,那么协程帧就会被销毁。销毁协程帧涉及以下步骤:

  1. 调用promise对象的析构函数。
  2. 调用函数参数副本的析构函数。
  3. 使用操作符delete释放协程帧所使用的内存(可选)。
  4. 将执行转回调用者/恢复者。

当执行首次到达 co_await 表达式内的 <return-to-caller-or-resumer> 点,或者如果协程在没有遇到 <return-to-caller-or-resumer> 点的情况下运行完毕,那么协程要么被挂起要么被销毁,而之前从调用 promise.get_return_object() 返回的返回对象随后被返回给协程的调用者。

参考文献

[编辑]
  1. Asymmetric Transfer