理解C++20 Coroutine: Promise type
理解C++20 Coroutine: promise type
这是我学习 C++20 Coroutine 笔记的第三篇,打算做成一个系列,如果感兴趣可以从头开始读:
本文的主要参考文献是 C++ Coroutines: Understanding the promise type,建议直接读原文而不是我的笔记。
Promise objects
前面的笔记提到过,Promise object 是用来控制协程的行为的。
promise type 的实例 promise object 会在每次调用协程时被创建。然后编译器会为协程函数的调用生成一些代码,在协程执行到特定点时调用 promise 特定的函数。
举例说明,假设有一个协程被调用之后,创建了一个 promise object: promise,那么对于这个协程的调用可能会是这样的:
1 | { |
其中 <body-statements>
是协程的代码。
相比于普通函数调用,协程的调用会额外多一些步骤:
- 通过 operator new 分配协程帧(coroutine frame);(可选步骤)
- 复制所有函数参数到协程帧;
- 调用 promise type 的构造函数,得到 promise object: promise。
- 调用 promise.get_return_object() 来获取协程第一次返回时的返回结果;
- 调用 promise.initial_suspend() 并 co_await 它的返回结果;
- 当 co_await promise.initial_suspend() 恢复执行(resume),你所编写的协程主体代码开始执行。
当执行遇到 co_return 时执行的一些额外的步骤:
- 调用 promise.return_void() 或 promise.return_value(
<expr>
); - 以创造它们的反向顺序销毁所有局部变量;
- 调用 promise.final_suspend() 并 co_await 它的返回结果;
或者,执行因为遇到未处理异常而退出,那么:
- 捕获该异常并在 catch-block 调用 promise.unhandled_exception();
- 调用 promise.final_suspend() 并 co_await 它的返回结果;
一旦协程主体的代码执行完毕,协程帧就会被销毁,销毁步骤包括:
- 调用 promise object 的析构函数;
- 调用函数参数副本的析构函数;
- 调用 operator delete 来释放协程帧的内存;(可选步骤)
- 将执行迁移回 caller/resumer;
分配协程帧
传递给 operator new 的大小不是 sizeof(promise type),而是整个协程帧的大小,包括所有函数参数的大小、promise object 的大小、局部变量的大小和编译器用于管理协程状态的内存大小。
作为一种优化,编译器可能不会使用 operator new 分配协程帧。
- it is able to determine that the lifetime of the coroutine frame is strictly nested within the lifetime of the caller; and
- the compiler can see the size of coroutine frame required at the call-site.
在这种情况下,编译器可以将协程帧分配到调用者的激活帧中(栈帧(stack-frame) 或 协程帧(coroutine-frame) 都有可能)。
promise type 可以定义 operator new() 的重载来替换全局的 operator new。
复制参数到协程帧
如果参数以值传递的方式(pass-by-value),那么会调用移动构造函数复制到协程帧。
如果参数以引用传递的方式(pass-by-reference),无论是左值还是右值,都只有引用会被复制到协程帧。
构造 promise object
在复制所有参数到协程帧之后会构建 promise object,这种设计允许你在构造 promise object 时(构造函数内)可以访问这些参数。
获取返回对象
成功构造 promise object 后的第一件事情就是通过调用 promise.get_return_object() 获取返回对象。
理解C++20 Coroutine: Promise type
https://uint128.com/2022/03/02/理解C-20-Coroutine-promise-type/