写在前面
C++20无栈协程主要提供了2种接口定,Promise 接口和 Awaitable 接口。通过自定义这些接口从而自定义协程的行为。这篇文章简单介绍Awaitable接口。
Promise 接口指定用于自定义协程本身行为的方法。让库作者能够自定义调用协程时发生的事件,如协程返回时(通过正常方式或通过未处理的异常返回),或者自定义协程中任何
co_await
或co_yield
表达式的行为。Awaitable 接口指定控制
co_await
表达式语义的方法。当一个值为co_await
时,代码被转换为对 awaitable 对象上的方法的一系列调用。它可以指定:是否暂停当前协程,暂停调度协程以便稍后恢复后执行一些逻辑,还有在协程恢复后执行一些逻辑以产生co_await
表达式的结果。
co_await 语义
在介绍 co_await
之前,先来看看与之相关的2个概念:
awaitable
: 即支持co_await
的表达式awaiter
:awaiter
是awaitable
返回的对象,它有可能返回的是自己,所以一个awaitable
还可以是一个awaiter
。不过一般是一种实现三个特殊方法的类型,它们被称为co_await
表达式的一部分:await_ready
,await_suspend
和await_resume
。
之后就是简洁而又有点复杂的 co_await
,我认为对此最简单的做法是在reference中的example进行单步运行,就很容易理解啦。
co_await
的调用形式就是 co_await expression
。
首先,将expression
转换成 awaitable
:
- 如果表达式是
final_suspend
、initial_suspend
、yield_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
是表示当前 协程的协程句柄。- 如果
await_suspend
返回 void,那么立即将控制返回给当前协程的调用方/恢复方(此协程保持暂停),否则 - 如果
await_suspend
返回 bool,那么:- 值为 true 时将控制返回给当前协程的调用方/恢复方
- 值为 false 时恢复当前协程。
如果
await_suspend
返回某个其他协程的协程句柄,那么(通过调用handle.resume()
)恢复该句柄(注意这可以连锁进行,并最终导致当前协程恢复)如果
await_suspend
抛出异常,那么捕捉该异常,恢复协程,并立即重抛异常- 最后,调用
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++ - What does co_await operator actually do? - Stack Overflow