理解co_await语义

2023/04/23 协程 共 2559 字,约 8 分钟

写在前面

C++20无栈协程主要提供了2种接口定,Promise 接口和 Awaitable 接口。通过自定义这些接口从而自定义协程的行为。这篇文章简单介绍Awaitable接口。

Promise 接口指定用于自定义协程本身行为的方法。让库作者能够自定义调用协程时发生的事件,如协程返回时(通过正常方式或通过未处理的异常返回),或者自定义协程中任何 co_awaitco_yield 表达式的行为。

Awaitable 接口指定控制 co_await 表达式语义的方法。当一个值为 co_await 时,代码被转换为对 awaitable 对象上的方法的一系列调用。它可以指定:是否暂停当前协程,暂停调度协程以便稍后恢复后执行一些逻辑,还有在协程恢复后执行一些逻辑以产生 co_await 表达式的结果。

co_await 语义

在介绍 co_await 之前,先来看看与之相关的2个概念:

  1. awaitable: 即支持 co_await 的表达式
  2. awaiterawaiterawaitable返回的对象,它有可能返回的是自己,所以一个awaitable还可以是一个awaiter。不过一般是一种实现三个特殊方法的类型,它们被称为 co_await 表达式的一部分:await_readyawait_suspendawait_resume

之后就是简洁而又有点复杂的 co_await,我认为对此最简单的做法是在reference中的example进行单步运行,就很容易理解啦。

co_await的调用形式就是 co_await expression

首先,将expression转换成 awaitable

  • 如果表达式是 final_suspendinitial_suspendyield_value 的返回值,则 awaitable就是这个返回值
  • 否则,当前协程的承诺类型 Promise 拥有成员函数 await_transform,那么可等待体是 promise.await_transform(expression式)
  • 如果expression已经是 awaitable是表达式本身

然后以下列方式获得等待器(awaiter)对象:

  • 如果针对 operator co_await 的重载决议给出单个最佳重载,那么等待器是该调用的结果(对于成员重载为 awaitable.operator co_await();,对于非成员重载为 operator co_await(static_cast<Awaitable&&>(awaitable));)
  • 否则,如果重载决议找不到 operator co_await,那么等待器是可等待体本身
  • 否则,如果重载决议有歧义,那么程序非良构

总之,如果 promise 类型 P 有一个名为 await_transform 的成员,那么 <expr> 首先被传递给 promise.await_transform(<expr>) 以获得 Awaitable 的值。 否则,如果 promise 类型没有 await_transform 成员,那么我们使用直接评估 <expr> 的结果作为 Awaitable 对象。然后,如果 Awaitable 对象,有一个可用的运算符 co_await() 重载,那么调用它来获取 Awaiter 对象。 否则,awaitable 的对象被用作 awaiter 对象。

在获取了 awaiter 对象之后,就可以执行 co_await语句了,首先,调用 awaiter.await_ready()。

如果返回true :

  • 直接执行 awaiter.await_resume(),它的结果就是整个 co_await expr 表达式的结果。

如果返回false:

  • 暂停协程(以各局部变量和当前暂停点填充其协程状态)。然后,调用 awaiter.await_ready()(这是当已知结果就绪或可以同步完成时,用以避免暂停开销的快捷方式)。如果它的结果按语境转 换到 bool 的结果是 false,那么:暂停协程(以各局部变量和当前暂停点填充其协程状态)。之后调用 awaiter.await_suspend(handle),其中 handle 是表示当前 协程的协程句柄。

    1. 如果 await_suspend 返回 void,那么立即将控制返回给当前协程的调用方/恢复方(此协程保持暂停),否则
    2. 如果 await_suspend 返回 bool,那么:
      • 值为 true 时将控制返回给当前协程的调用方/恢复方
      • 值为 false 时恢复当前协程。
    3. 如果 await_suspend 返回某个其他协程的协程句柄,那么(通过调用 handle.resume())恢复该句柄(注意这可以连锁进行,并最终导致当前协程恢复)

    4. 如果 await_suspend 抛出异常,那么捕捉该异常,恢复协程,并立即重抛异常

    5. 最后,调用 awaiter.await_resume(),它的结果就是整个 co_await expr 表达式的结果。

如果协程在 co_await 表达式中暂停而在后来resume,那么恢复点处于紧接对 awaiter.await_resume() 的调用之前。

注意,因为协程在进入 awaiter.await_suspend() 前已经完全暂停,所以该函数可以自由地在线程间转移协程柄而无需额外同步。例如,可以将它放入回调,将它调度成在异步输入/输出操作完成时在线程池上运行等。

co_yield 语义

​ 再简单介绍一下 co_yield 语义,

co_yield 表达式		
co_yield 花括号初始化器列表	
被转换为
co_await promise.yield_value(表达式)

​ 典型的生成器的 yield_value 会将其实参存储(复制/移动或仅存储它的地址,因为实参的生存期跨过 co_await 内的暂停点)到生成器对象中并返回 std::suspend_always,将控制转移给调用方/恢复方。

Reference

协程 (C++20) - cppreference.com

c++ - What does co_await operator actually do? - Stack Overflow

C++ Coroutines: Understanding the Compiler Transform)

purecpp - a cool open source modern c++ community

Search

    Table of Contents