C++协程(5):理解编译器转换
介绍
之前的“理解C++协程”文章中讲到了编译器会对协程,以及它的 $co_-await$、$co_-yield$、$co_-return$ 表达式执行的不同类型的转换。这些文档描述了编译器如何将每个表达式翻译成底层的对多种自定义的点 / 用户定义的方法调用。
然而,这些描述还有一部分你可能不太满意。它们对于“挂起点”的概念一笔带过,含糊地说成“协程在这里挂起”、“协程在这里恢复”,并没有详细说明它的含义,以及编译器是怎么实现的。
在这篇文章,我会把之前文章的所有概念更深入地讲解。我会把一个协程转换为底层等价的非协程、命令式的C++
代码,来展示当协程执行到挂起点时发生了什么。
注意我不会描述某个特定编译器怎么把协程编译成机器码 ( 编译器在这方面会有些额外技巧 ),而是只描述一种可能的把协程转化成可移植的C++
代码的方式。
警告:这次讨论会有点深!
设置场景
对于初学者,我们假设有一个基本的 $task$ 类型,同时作为 $awaitable$ 和协程的返回类型。为了简化,假设这个协程类型异步生成一个 $int$ 类型结果。
在这篇文章,我们会描述怎么把下面的协程代码转换为不包含任何协程关键字 $co_-await$、$co_-return$的C++
代码,以便我们更好理解它的含义。
// Forward declaration of some other function. Its implementation is not relevant.
task f(int x);
// A simple coroutine that we are going to translate to non-C++ code
task g(int x) {
int fx = co_await f(x);
co_return fx * fx;
}
定义 task 类型
首先,我们声明一个会被用到的 $task$ 类。
为了理解协程怎么转化成底层代码,我们不需要明白这个类型的方法定义。对它们的调用会被插入转换中。
这些方法的定义并不复杂,我会把它们作为读者理解以前文章的练习实践。
class task {
public:
struct awaiter;
class promise_type {
public:
promise_type() noexcept;
~promise_type();
struct final_awaiter {
bool await_ready() noexcept;
std::coroutine_handle<> await_suspend(
std::coroutine_handle<promise_type> h) noexcept;
void await_resume() noexcept;
};
task get_return_object() noexcept;
std::suspend_always initial_suspend() noexcept;
final_awaiter final_suspend() noexcept;
void unhandled_exception() noexcept;
void return_value(int result) noexcept;
private:
friend task::awaiter;
std::coroutine_handle<> continuation_;
std::variant<std::monostate, int, std::exception_ptr> result_;
};
task(task&& t) noexcept;
~task();
task& operator=(task&& t) noexcept;
struct awaiter {
explicit awaiter(std::coroutine_handle<promise_type> h) noexcept;
bool awaite_ready() noexcept;
std::coroutine_handle<promise_type> await_suspend(
std::coroutine_handle<> h) noexcept;
int await_resume();
private:
std::coroutine_handle<promise_type> coro_;
};
awaiter operator co_await() && noexcept;
private:
explicit task(std::coroutine_handle<promise_type> h) noexcept;
std::coroutine_handle<promise_type> coro_;
};
这个 $task$ 类型的定义与理解 $promise$的相似。
第一步:确定 promise 类型
task g(int x) {
int fx = co_await f(x);
co_return fx * fx;
}
当编译器看到这个函数包含三个协程关键字 ( $co_-await$、$co_-yield$、$co_-return$ ) 中的一个时,它开始执行协程转换步骤。
第一步是确定协程的 $promise$ 类型。
这一步是通过把函数签名的返回类型和参数类型作为 $std$::$coroutine_traits$ 类型的模板参数判断的。
例如,我们的函数 $g$,返回类型为 $task$,有一个参数类型是 $int$,编译器会使用 $std$::$coroutine_-traits$<$task$, $int$>::$promise_-type$ 判断。
让我们定义个别名,方便以后使用。
using __g_promise_t = std::coroutine_traits<task, int>::promise_type;
注意:我使用了两个下划线开始的类型名,表示这个符号是编译器内部生成的。这种符号是实现时保留的,并且不应该在你的代码中使用。
现在,因为我们没有特例化 $std$::$coroutine_-traits$,这会导致主模板实例化,而主模板只是把内嵌的 $promise_-type$ 作为返回类型的 $promise_-type$ 的别名,在我们的例子中也就是解析为 $task$::$promise_-type$。
第二步:创建协程状态
一个协程函数需要在挂起时保存协程状态、参数和局部变量,以便在后续恢复时保持可用。
这个状态,在C++
标准中,被称为协程状态,通常分配在堆上。
让我们开始给协程 $g$ 定义个协程状态结构。
struct __g_state {
// to be filled out
};
协程状态包含了一系列不同的东西:
- $promise$ 对象
- 所有函数参数的副本
- 协程当前挂起所在的挂起点信息,以及如何恢复 / 销毁它
- 所有局部变量 / 跨挂起点的临时变量存储
让我们加上 $promise$ 对象和参数副本存储。
struct __g_state {
int x;
__g_promise_t __promise;
// to be filled out
};
接着,我们加个构造函数来初始化这些数据成员。
回想一下,编译器会先尝试使用参数副本的左值引用来调用 $promise$ 的构造函数,如果不行,就会调用 $promise$ 的默认构造函数。
让我们创建一个简单的 $helper$ 来帮忙:
template<typename Promise, typename... Params>
Promise construct_promise([[maybe_unused]] Params&... params) {
if constexpr (std::constructible_from<Promise, Params&...>) {
return Promise(params...);
} else {
return Promise();
}
}
因此,协程状态构造器看起来可能会像这样:
struct __g_state {
__g_state(int&& x)
: x(static_cast<int&&>(x))
, __promise(construct_promise<__g_promise_t>(this->x))
{}
int x;
__g_promise_t __promise;
// to be filled out
};
现在我们有了一个表示协程状态的类型雏形,我们可以通过在堆上分配一个 __$g_-state$ 实例,传递函数参数给它拷贝 / 移动的方式,构建出 $g\left(\right)$ 底层实现的雏形。
一些术语:我使用“启动函数” ( $ramp$ $function$ ) 指代协程初始化协程状态和准备开始执行协程的逻辑,就像一条进入协程体执行的坡道。
task g(int x) {
auto* state = new __g_state(static_cast<int&&>(x));
// ... implement rest of the ramp function
}
注意我们的 $promise$ 类型没有重载 $operator$ $new$,所以我们会调用全局的 ::$operator$ $new$。
如果 $promise$ 类型重载了 $operator$ $new$,我们不会调用全局的 ::$operator$ $new$,而是先确认参数列表 $\left(size, paramLvalues…\right)$ 是否可以作为 $operator$ $new$ 的参数,如果可以的话,就使用这个参数列表调用;否则,我们只会使用 $\left(size\right)$ 参数列表调用。$operator$ $new$ 的访问协程函数参数列表的能力有时被称为“参数预览” ( $parameter$ $preview$ ),在你想要使用 $allocator$ 作为参数来给协程状态分配空间时很有用。
如果编译器发现 __$g_-promise_-t$::$operator$ $new$ 的实现,逻辑会被转换为以下:
template<typename Promise, typename... Args>
void* __promise_allocate(std::size_t size, [[maybe_unused]] Args&... args) {
if constexpr (requires { Promise::operator new(size, args...); }) {
return Promise::operator new(size, args...);
} else {
return Promise::operator new(size);
}
task g(int x) {
void* state_mem = __promise_allocate<__g_promise_t>(sizeof(__g_state), x);
__g_state* state;
try {
state = ::new (state_mem) __g_state(static_cast<int&&>(x));
} catch (...) {
__g_promise_t::operator delete(state_mem);
throw;
}
// ... implement rest of the ramp function
}
}
同样,这个 $promise$ 类型没有定义静态函数成员 $get_-return_-object_-on_-allocation_-failure\left(\right)$。如果 $promise$ 类型定义了这个函数,这里的分配会使用 $std$::$nothrow_-t$ 形式的 $operator$ $new$,并且当分配返回 $nullptr$ 时返回 __$g_-promise_-t$::$get_-return_-object_-on_-allocation_-failure\left(\right)$。
即,看起来会像这样:
task g(int x) {
auto* state = ::new (std::nothrow) __g_state(static_cast<int&&>(x));
if (state == nullptr) {
return __g_promise_t::get_return_object_on_allocation_failure();
}
// ... implement rest of the ramp function
}
为了简化剩下的例子,我们使用最简单的调用全局 ::$operator$ $new$ 内存分配函数。
第三步:调用 get_return_object()
启动函数要做的下一件事是调用 $promise$ 对象的 $get_-return_-object\left(\right)$ 方法,获取启动函数的返回值。
返回值会作为局部变量存储,并在启动函数结束返回 ( 在其他步骤完成后 )。
task g(int x) {
auto* state = new __g_state(static_cast<int&&>(x));
decltype(auto) return_value = state->__promise.get_return_object();
// ... implement rest of ramp function
return return_value;
}
然而,$get_-return_-object\left(\right)$ 的调用可能抛出异常,这时我们想要释放协程状态分配的内存。一种比较好的方法是使用 $std$::$unique_-ptr$ 来管理,这样在后续操作抛出异常时,它会被释放:
task g(int x) {
std::unique_ptr<__g_state> state(new __g_state(static_cast<int&&>(x)));
decltype(auto) return_value = state->__promise.get_return_object();
// ... implement rest of ramp function
return return_value;
}
第四步:初始化挂起点
启动函数在调用 $get_-return_-object\left(\right)$ 之后的下一件事是开始执行协程体,协程体执行的第一件事是初始化挂起点,即等价于 $co_-await$ $promise.initial_-suspend\left(\right)$。
现在,理想的话,我们只需要让协程初始化挂起,然后实现恢复一个初始化挂起协程的启动。然而,初始化挂起点在处理协程以及协程状态的生命周期问题上,有一些奇怪的细节。这些是C++20
发布前对初始化挂起点语义的一些后期调整,目的是修复一些存在的问题。
根据初始化挂起点的定义,如果一个异常从这些地方抛出:
- $initial_-suspend\left(\right)$ 的调用,
- 对返回的 $awaitable$ 的 $operator$ $co_-await\left(\right)$ ( 如果有定义 ),
- 调用 $awaiter$ 的 $await_-ready\left(\right)$,或者
- 调用 $awaiter$ 的 $await_-suspend\left(\right)$
那么异常会传播到启动函数的调用方,协程状态会被自动销毁。
如果一个异常从以下地方抛出:
- $await_-resume\left(\right)$ 调用,
- $operator co_-await\left(\right)$ 返回对象的析构函数 ( 如果可用 ),或者
- $initial_-suspend\left(\right)$ 返回对象的析构函数
那么异常会被协程体捕获,然后调用 $promise.unhandled_-exception\left(\right)$。
这意味着我们需要注意这部分的转换,一部分需要实现在启动函数,另一部分需要放在协程体。
而且,因为 $initial_-suspend\left(\right)$ 返回的对象和 $operator$ $co_-await$ 返回的对象 ( 可选的 ) 具有跨挂起点的生命周期 ( 它们会在协程挂起点前创建,恢复后销毁 ),这些对象的存储需要放在协程状态里。
在我们特别的例子中,$initial_-suspend\left(\right)$ 返回类型是 $std$::$suspend_-always$,后者是一个空的、平凡构造类型。然而,逻辑上我们还是需要在协程状态里保存这个对象的实例,所以我们会一直加上这部分存储,展示它如何工作。
这个对象只在调用 $initial_-suspend\left(\right)$ 的位置构造,所以我们需要新增一个确定类型的数据成员,允许我们显示地控制它的生命周期。
为了支持这些功能,我们先定义一个 $helper$ 类 $manual_-lifetime$,后者具有平凡的构造和析构函数,我们会在需要的时候显式地构造 / 析构存储在其中的值:
template<typename T>
struct manual_lifetime {
manual_lifetime() noexcept = default;
~manual_lifetime() = default;
// Not copyable/movable
manual_lifetime(const manual_lifetime&) = delete;
manual_lifetime(manual_lifetime&&) = delete;
manual_lifetime& operator=(const manual_lifetime&) = delete;
manual_lifetime& operator=(manual_lifetime&&) = delete;
template<typename Factory>
requires
std::invocable<Factory&> &&
std::same_as<std::invoke_result_t<Factory&>, T>
T& construct_from(Factory factory) noexcept(std::is_nothrow_invocable_v<Factory&>) {
return *::new (static_cast<void*>(&storage)) T(factory());
}
void destroy() noexcept(std::is_nothrow_destructible_v<T>) {
std::destroy_at(std::launder(reinterpret_cast<T*>(&storage)));
}
T& get() & noexcept {
return *std::launder(reinterpret_cast<T*>(&storage));
}
private:
alignas(T) std::byte storage[sizeof(T)];
};
注意 $construct_-from\left(\right)$ 方法设计成接受lambda
而不是接受构造函数参数。这让我们可以使用拷贝消除特性,就地通过函数调用结果初始化构造对象。如果使用构造函数参数,我们就需要进行一次不必要的移动构造函数。
现在我们可以通过 $manual_-lifetime$ 声明 $promise.initial_-suspend\left(\right)$ 返回的临时对象数据成员。
struct __g_state {
__g_state(int&& x);
int x;
__g_promise_t __promise;
manual_lifetime<std::suspend_always> __tmp1;
// to be filled out
};
$std$::$suspend_-always$ 类型没有定义 $operator$ $co_-await\left(\right)$,所以我们不需要额外的内存保存其返回的临时对象。
一旦我们通过 $initial_-suspend\left(\right)$ 构造这个对象,我们就需要调用 $await_-ready\left(\right)$、$await_-suspend\left(\right)$ 和 $await_-resume\left(\right)$ 实现 $co_-await$ 表达式。
当调用 $await_-suspend\left(\right)$ 时,我们需要传递当前协程句柄。现在我们可以通过直接把 $promise$ 引用作为参数调用 $std$::$coroutine_-handle$<__$g_-promise_-t$>::$from_-promise\left(\right)$ 的方式实现。我们稍后再看看它的内部结构。
同样,$.await_-suspend\left(handle\right)$ 调用的结果类型是 $void$,因此不需要像 $bool$ 返回或者 $coroutine_-handle$ 返回那样考虑是否恢复当前协程或者另一个协程。
最终,所有 $std$::$suspend_-always$ $awaiter$ 的方法都是 $noexcept$ 的,我们不需要担心异常。如果它们可能会抛出异常,我们需要添加额外的代码保证临时的 $std$::$supsend_-always$ 对象在异常传播到启动函数之外前被销毁。
一旦 $await_-suspend\left(\right)$ 成功返回或者协程体准备开始执行时,如果有异常抛出,我们就不再需要自动销毁协程状态。所以我们可以调用持有协程状态的 $std$::$unique_-ptr$ 的 $release\left(\right)$ 来避免当我们从函数返回时协程状态的自动销毁。
所以现在我们可以实现初始化挂起表达式的第一部分:
task g(int x) {
std::unique_ptr<__g_state> state(new __g_state(static_cast<int&&>(x)));
decltype(auto) return_value = state->__promise.get_return_object();
state->__tmp1.construct_from([&]() -> decltype(auto) {
return state->__promise.initial_suspend();
});
if (!state->__tmp1.get().await_ready()) {
//
// ... suspend-coroutine here
//
state->__tmp1.get().await_suspend(
std::coroutine_handle<__g_promise_t>::from_promise(state->__promise));
state.release();
// fall through to return statement below.
} else {
// Coroutine did not suspend.
state.release();
//
// ... start executing the coroutine body
//
}
return __return_val;
}
$await_-resume\left(\right)$ 调用和 __$tmp1$ 的析构函数会在协程体出现,所以不会在启动函数出现。
现在我们有了一个 ( 大部分 ) 功能等价的初始化挂起点逻辑,但启动函数仍然有一系列 TODO。为了解决这些,我们首先需要绕道研究一下挂起然后恢复一个协程的策略。
第五步:记录挂起点
当协程挂起时,需要确保恢复点与之前的挂起点相同。
此外,还需要跟踪每个挂起点上自动生命周期对象的存活状态,以便之后协程如果被销毁而不是挂起时,知道需要销毁哪些对象。
一种实现方式是给每个挂起点一个唯一编号,然后在协程状态使用一个整型数据成员存储。
无论什么时候,当一个协程挂起,它需要把当前挂起点编号写入协程状态,在之后恢复 / 销毁的时候,根据这个整型来找到之前的挂起点。
注意这不是通过协程状态存储挂起点的唯一方法,然而,主流的三个编译器 ( MSVC
、Clang
和GCC
) 在这篇文章发布 ( $2022$ 年 ) 的时候都采用了这个方法。另一个可能的解决办法是每个挂起点都使用不同的恢复 / 销毁函数指针,但这篇文章不会讲解这个办法。
那么让我们来扩展协程状态,使用一个整型数据成员来保存挂起点下标,并初始化为 $0$ ( 我们把使用值表示初始化挂起点 )。
struct __g_state {
__g_state(int&& x);
int x;
__g_promise_t __promise;
int __suspend_point = 0; // <-- add the suspend-point index
manual_lifetime<std::suspend_always> __tmp1;
// to be filled out
};
第六步:实现 coroutine_handle::resume() 和 coroutine_handle::destory()
当协程被 $coroutine_-handle$::$resume\left(\right)$ 调用恢复时,我们需要调用某些函数实现被挂起协程的剩余部分,被调用的函数查找挂起点下标,然后跳转到控制流中的适当位置。
此外,我们需要实现 $coroutine_-handle$::$destroy\left(\right)$ 函数,通过合适的逻辑销毁当前挂起点作用内的对象。然后我们需要实现 $coroutine_-handle$::$done\left(\right)$ 来确认当前挂起点是否为最终挂起点。
$coroutine_-handle$ 方法接口不知道协程状态的具体类型,$coroutine_-handle$<$void$> 类型可以指向任何协程实例。这意味着我们需要以类型被擦除的协程状态实现。
我们可以存储指向协程类型的恢复 / 销毁函数指针,并让 $coroutine_-handle$::$resume$ / $destroy\left(\right)$ 调用这些函数指针。
$coroutine_-handle$ 类型同样需要实现通过 $coroutine_-handle$::$address\left(\right)$ 转换为 $void*$,和通过 $coroutine_-handle$::$from_-address\left(\right)$ 转换为 $void*$。
进一步,协程可以被任意一个指向它的句柄恢复 / 销毁,不只是最近一个传给 $await_-suspend\left(\right)$ 调用的。
这些要求让我们定义的 $coroutine_-handle$ 类型只能包含一个指向协程状态的指针,在状态里通过数据成员存储恢复 / 销毁函数指针,而不是把恢复 / 销毁函数指针存在 $coroutine_-handle$ 里。
同样,因为我们需要 $coroutine_-handle$ 能够指向任意协程状态对象,所以所有协程状态类型的函数指针数据成员应该保持一致。
一种直接方法是让协程状态类型继承一些包含这些数据成员的基类。
例如,我们可以定义以下类型作为所有协程状态类型的基类:
struct __coroutine_state {
using __resume_fn = void(__coroutine_state*);
using __destroy_fn = void(__coroutine_state*);
__resume_fn* __resume;
__destroy_fn* __destroy;
};
这样 $coroutine_-handle$::$resume\left(\right)$ 可以直接叫 __$resume\left(\right)$,使用 __$coroutine_-state$ 指针作为参数。$coroutine_-handle$::$destroy\left(\right)$ 和 __$destroy\left(\right)$ 函数指针也是一样。
对于 $coroutine_-handle$::$done\left(\right)$ 方法,我们选择一个空 __$resume\left(\right)$ 函数指针表示最终挂起点。这种方法很方便,因为最终挂起点不支持 $resume\left(\right)$,只支持 $destroy\left(\right)$。如果尝试对一个在最终挂起点挂起的协程调用 $resume\left(\right)$ ( 这是未定义行为 ),那么会调用一个空函数指针,这会失败并且能快速指出错误。
基于这些,我们可以实现 $coroutine_-handle$<$void$>:
namespace std
{
template<typename Promise = void>
class coroutine_handle;
template<>
class coroutine_handle<void> {
public:
coroutine_handle() noexcept = default;
coroutine_handle(const coroutine_handle&) noexcept = default;
coroutine_handle& operator=(const coroutine_handle&) noexcept = default;
void* address() const {
return static_cast<void*>(state_);
}
static coroutine_handle from_address(void* ptr) {
coroutine_handle h;
h.state_ = static_cast<__coroutine_state*>(ptr);
return h;
}
explicit operator bool() noexcept {
return state_ != nullptr;
}
friend bool operator==(coroutine_handle a, coroutine_handle b) noexcept {
return a.state_ == b.state_;
}
void resume() const {
state_->__resume(state_);
}
void destroy() const {
state_->__destroy(state_);
}
bool done() const {
return state_->__resume == nullptr;
}
private:
__coroutine_state* state_ = nullptr;
};
}
第七步:实现 coroutine_handle<Promise>::promise() 和 from_promise()
对于更通用的 $coroutine_-handle$<$Promise$> 特例化,大部分实现都可以复用 $coroutine_-handle$<$void$> 实现。然而,我们也需要通过 $promise\left(\right)$ 方法的返回,访问协程状态的 $promise$ 对象,以及从 $promise$ 对象引用构造出一个 $coroutine_-handle$ 。
然而,因为 $coroutine_-handle$<$Promise$> 类型必须能指向任何 $promise$ 类型是 $Promise$ 的协程状态,因此我们无法简单从指针获取具体协程类型。
我们需要定义一个新的协程状态基类继承 __$coroutine_-state$,前者应当包含 $promise$ 对象,这样我们就可以定义所有使用特定 $promise$ 类型的协程状态类型,并让它们继承基类。
template<typename Promise>
struct __coroutine_state_with_promise : __coroutine_state {
__coroutine_state_with_promise() noexcept {}
~__coroutine_state_with_promise() {}
union {
Promise __promise;
};
};
你可能好奇为什么 __$promise$ 成员会定义在一个匿名的 $union$ 里面……
原因是派生类是为特定的包含参数副本数据成员的协程函数创建的。派生类的数据成员默认会在所有基类数据成员后初始化,所以把 $promise$ 对象声明为普通数据成员意味着它会在参数副本数据成员之前被构造。
然而,我们需要 $promise$ 的构造函数在参数副本构造函数之后调用,因为参数副本引用可能会被传给 $promise$ 构造函数。
因此,我们在基类中为 $promise$ 对象保留空间,这样它们就在协程状态内部有一个一致的偏移量,并在参数副本被初始化后的合适点位,调用派生类的构造 / 析构函数。这种控制是通过把 $promise$ 声明为 $union$ 成员实现的。
让我们更新 __$g_-state$ 类型,继承新的基类。
struct __g_state : __coroutine_state_with_promise<__g_promise_t> {
__g_state(int&& __x)
: x(static_cast<int&&>(__x)) {
// Use placement-new to intialise the promise object in the base-class
::new ((void*)std::addressof(this->__promise))
__g_promise_t(construct_promise<__g_promise_t>(x));
}
~__g_state() {
// Also need to manually call the promise destructor before the
// argument objects are destroyed.
this->__promise.~__g_promise_t();
}
int __suspend_point = 0;
int x;
manual_lifetime<std::suspend_always> __tmp1;
// to be filled out
}
现在我们已经定义了 $promise$ 基类,可以开始实现 $std$::$coroutine_-handle$<$Promise$> 类模板。
大部分实现都很像 $coroutine_-handle$<$void$>,除了使用 __$coroutine_-state_-with_-promise$<$Promise$> 指针而不是 __$coroutine_-state$ 指针。
新增部分只有 $promise\left(\right)$ 和 $from_-promise\left(\right)$ 函数。
- $promise\left(\right)$ 方法直接返回协程状态的 __$promise$ 成员。
- $from_-promise\left(\right)$ 方法需要我们从 $promise$ 对象地址计算出协程状态对象地址。我们只需要从 $promise$ 对象地址中减去 __$promise$ 成员的偏移量就可以了。
$coroutine_-handle$<$Promise$> 的实现是:
namespace std
{
template<typename Promise>
class coroutine_handle {
using state_t = __coroutine_state_with_promise<Promise>;
public:
coroutine_handle() noexcept = default;
coroutine_handle(const coroutine_handle&) noexcept = default;
coroutine_hanlde& operator=(const coroutine_handle&) noexcept = default;
operator coroutine_handle<void>() const noexcept {
return coroutine_handle<void>::from_address(address());
}
explicit operator bool() const noexcept {
return state_ != nullptr;
}
friend bool operator==(coroutine_handle a, coroutine_handle b) noexcept {
return a.state_ == b.state_;
}
void* address() const {
return static_cast<void*>(static_cast<__coroutine_state*>(state_));
}
static coroutine_handle from_address(void* ptr) {
coroutine_handle h;
h.state_ = static_cast<state_t*>(static_cast<__coroutine_state*>(ptr));
return h;
}
Promise& promise() const {
return state_->__promise;
}
static coroutine_handle from_promise(Promise& promise) {
coroutine_handle h;
// We know the address of the __promise member, so calculate the
// address of the coroutine-state by subtracting the offset of
// the __promise field from this address.
h.state_ = reinterpret_cast<state_t*>(
reinterpret_cast<unsigned char*>(std::addressof(promise) -
offsetof(state_t, __promise)));
return h;
}
// Define these in terms of their coroutine_handle<void> implementations
void resume() const {
static_cast<coroutine_handle<void>>(*this).resume();
}
void destroy() const {
static_cast<coroutine_handle<void>>(*this).destroy();
}
bool done() const {
return static_cast<coroutine_handle<void*>>(*this).done();
}
}
private:
state_t* state_;
}
现在已经定义了协程恢复机制,我们可以回到启动函数,并实现初始化我们往协程状态新加的函数指针数据成员。
第八步:函数体开始部分
现在我们前向声明具有正确签名的恢复/销毁函数,并更新 __$g_-state$ 构造函数来初始化协程状态,以便恢复/销毁函数指针指向它们:
void __g_resume(__coroutine_state* s);
void __g_destroy(__coroutine_state* s);
struct __g_state : __coroutine_state_with_promise<__g_promise_t> {
__g_state(int&& __x)
: x(static_cast<int&&>(__x)) {
// Initialise the function-pointers used by coroutine_handle methods.
this->__resume = &__g_resume;
this->__destroy = &__g_destroy;
// Use placement-new to intialise the promise object in the base-class
::new ((void*)std::addressof(this->__promise))
__g_promise_t(construct_promise<__g_promise_t>(x));
}
// ... rest omitted for brevity
};
task g(int x) {
std::unique_ptr<__g_state> state(new __g_state(static_cast<int&&>(x)));
decltype(auto) return_value = state->__promise.get_return_object();
state->__tmp1.construct_from([&]() -> decltype(auto) {
return state->__promise.initial_suspend();
});
if (!state->__tmp1.get().await_ready()) {
state->__tmp1.get().await_suspend(
std::coroutine_handle<__g_promise_t>::from_promise(state->__promise));
state.release();
// fall through to return statement below.
} else {
// Coroutine did not suspend. Start executing the body immediately.
__g_resume(state.release());
}
return return_value;
}
完成了启动函数部分后,我们可以开始看 $g\left(\right)$ 的恢复/销毁函数。
让我们继续转换初始化挂起表达式。
当 __$g_-resume\left(\right)$ 被调用且 __$suspend_-point$ 的下标是 $0$,我们就需要调用 __$tmp1$ 的 $await_-resume\left(\right)$,并在之后调用 $tmp1$ 的析构函数。
void __g_resume(__coroutine_state* s) {
// We know that 's' points to a __g_state.
auto* state = static_cast<__g_state*>(s);
// Generate a jump-table to jump to the correct place in the code based
// on the value of the suspend-point index.
switch (state->__suspend_point) {
case 0: goto suspend_point_0;
default: std::unreachable();
}
suspend_point_0:
state->__tmp1.get().await_resume();
state->__tmp1.destroy();
// TODO: Implement rest of coroutine body.
//
// int fx = co_await f(x);
// co_return fx * fx;
}
当 __$g_-destroy\left(\right)$ 被调用且 __$suspend_-point$ 下标是 $0$,我们要在销毁和释放协程状态前销毁 __$tmp1$。
void __g_destroy(__coroutine_state* s) {
auto* state = static_cast<__g_state*>(s);
switch (state->__suspend_point) {
case 0: goto suspend_point_0;
default: std::unreachable();
}
suspend_point_0:
state->__tmp1.destroy();
goto destroy_state;
// TODO: Add extra logic for other suspend-points here.
destroy_state:
delete state;
}
第九步:转换 co_await
表达式
接着,我们看看怎么转换 $co_-await$ $f\left(x\right)$ 表达式。
首先我们来看看一个返回 $task$ 临时对象的 $f\left(x\right)$。
因为临时的 $task$ 直到语句结尾的分号才会被销毁,而且语句含有 $co_-await$ 表达式,$task$ 的生命周期会扩散出挂起点,因此它需要存储在协程状态中。
当计算这个临时 $task$ 的 $co_-await$ 表达式时,我们需要调用返回临时 $awaiter$ 对象的 $operator$ $co_-await\left(\right)$ 方法。这个对象的生命周期也扩散出挂起点,所以也要保存在协程状态中。
让我们给 __$g_-state$ 类型加上必要成员:
struct __g_state : __coroutine_state_with_promise<__g_promise_t> {
__g_state(int&& __x);
~__g_state();
int __suspend_point = 0;
int x;
manual_lifetime<std::suspend_always> __tmp1;
manual_lifetime<task> __tmp2;
manual_lifetime<task::awaiter> __tmp3;
};
然后我们可以更新 __$g_-resume\left(\right)$ 函数来初始化这些临时对象,接着再计算 $co_-await$ 表达式包含的 $await_-ready$、$await_-suspend$ 和 $await_-resume$ 这 $3$ 个调用。
注意 $task$::$awaiter$::$await_-suspend\left(\right)$ 方法返回协程句柄,因此我们需要生成恢复返回句柄的代码。
我们也需要再调用 $await_-suspend\left(\right)$ 之前更新挂起点下标 ( 使用下标 $1$ ),然后给跳表添加额外的条目,确保在正确的位置恢复。
void __g_resume(__coroutine_state* s) {
// We know that 's' points to a __g_state.
auto* state = static_cast<__g_state*>(s);
// Generate a jump-table to jump to the correct place in the code based
// on the value of the suspend-point index.
switch (state->__suspend_point) {
case 0: goto suspend_point_0;
case 1: goto suspend_point_1; // <-- add new jump-table entry
default: std::unreachable();
}
suspend_point_0:
state->__tmp1.get().await_resume();
state->__tmp1.destroy();
// int fx = co_await f(x);
state->__tmp2.construct_from[&] {
return f(state->x);
};
state->__tmp3.construct_from([&] {
return static_cast<task&&>(state->__tmp2.get()).operator co_await();
});
if (!state->__tmp3.get().await_ready()) {
// mark the suspend-point
state->__suspend_point = 1;
auto h = state->__tmp3.get().await_suspend(
std::coroutine_handle<__g_promise_t>::from_promise(state->__promise));
// Resume the returned coroutine_handle before returning.
h.resume();
return;
}
suspend_point_1:
int fx = state->__tmp3.get().await_resume();
state->__tmp3.destroy();
state->__tmp2.destroy();
// TOOD: Implement
// co_return fx * fx;
}
注意 $int$ $fx$ 局部变量生命周期没有扩散出挂起点,因此它不需要存储在协程状态中。我们可以把它作为一个普通的 __$g_-resume$ 函数局部变量。
我们也需要给 __$g_-destroy\left(\right)$ 函数添加必要的条目来处理协程在挂起点销毁的情况。
void __g_destroy(__coroutine_state* s) {
auto* state = static_cast<__g_state*>(s);
switch (state->__suspend_point) {
case 0: goto suspend_point_0;
case 1: goto suspend_point_1; // <-- add new jump-table entry
default: std::unreachable();
}
suspend_point_0:
state->__tmp1.destroy();
goto destroy_state;
suspend_point_1:
state->__tmp3.destroy();
state->__tmp2.destroy();
goto destroy_state;
// TODO: Add extra logic for other suspend-ponits here.
destroy_state:
delete state;
}
这样我们就完成了语句:
int fx = co_await f(x);
然而,函数 $f\left(\right)$ 并没有标为noexcept
,意味着它可能抛出异常。同样,$awaiter$::$await_-resume\left(\right)$ 方法也没有标为noexcept
,也可能抛出异常。
当协程体抛出异常时,编译器生成代码来捕获,然后调用 $promise$.$unhandled_-exception\left(\right)$ 来给 $promise$ 机会对异常做些事情。我们来看看这方面的实现。
第十步:实现 unhandled_exception()
协程定义规范 dcl.fct.def.coroutine 中说,协程的行为就好像它的函数体被替换为:
{
promise-type promise promise-constructor-arguments;
try {
co_await promise.initial_suspend();
function-body
} catch (...) {
if (!intiali-await-resume-called)
throw;
promise.unhandled_exception();
}
final-suspend:
co_await promise.final_suspend();
}
我们已经单独处理了启动函数中的 $initial-await_-resume-called$ 分支,所以我们不需要关注这块。
来让我们调整 __$g_-resume\left(\right)$ 函数体,插入try/catch
块。
注意我们需要小心放置 $switch$,跳转到try
块的正确位置,因为我们不允许通过 $goto$ 进入一个try
块。
同样,我们需要谨慎地对 $await_-suspend\left(\right)$ 返回的在try/catch
块外的协程句柄调用 $.resume\left(\right)$。如果一个返回的协程的 $.resume\left(\right)$ 抛出了异常,它不应被当前协程捕获,而是传播给调用 $resume\left(\right)$ 恢复它的协程。因此我们在函数开头声明一个存储协程句柄的变量,然后 $goto$ 到try/catch
之外的位置,执行 $.resume\left(\right)$ 调用。
void __g_resume(__coroutine_state* s) {
auto* state = static_cast<__g_state*>(s);
std::coroutine_handle<void> coro_to_resume;
try {
switch (state->__suspend_point) {
case 0: goto suspend_point_0;
case 1: goto suspend_point_1; // <-- add new jump-table entry
default: std::unreachable();
}
suspend_point_0:
state->__tmp1.get().await_resume();
state->__tmp1.destroy();
// int fx = co_await f(x);
state->__tmp2.construct_from([&] {
return f(state->x);
});
state->__tmp3.construct_from([&] {
return static_cast<task&&>(state->__tmp2.get()).operator co_await();
});
if (!state->__tmp3.get().await_ready()) {
state->__suspend_point = 1;
coro_to_resume = state->__tmp3.get().await_suspend(
std::coroutine_handle<__g_promise_t>::from_promise(state->__promise));
goto resume_coro;
}
suspend_point_1:
int fx = state->__tmp3.get().await_resume();
state->__tmp3.destroy();
state->__tmp2.destroy();
// TODO: Implement
// co_return fx * fx;
} catch (...) {
state->__promise.unhandled_exception();
goto final_suspend;
}
final_suspend:
// TODO: Implement
// co_await promise.final_suspend();
resume_coro:
coro_to_resume.resume();
return;
}
上面的代码有个 bug。当 __$tmp3$.$get\left(\right)$.$await_-resume\left(\right)$ 调用因为异常退出,我们会在没有调用 __$tmp3$ 和 __$tmp2$ 的析构函数情况下捕获异常。
注意我们不能简单地捕获异常,调用析构函数然后重新抛出异常,因为这样会改变这些析构函数的行为,相当于在调用 $std$::$unhandled_-exceptions\left(\right)$ 前“处理”了异常。如果在异常展开时调用析构函数,那么 $std$::$unhandled_-exceptions\left(\right)$ 应该返回非零值。
我们可以定义个RAII
辅助类确保当异常抛出,退出作用域时析构函数被调用。
template<typename T>
struct destructor_guard {
explicit destructor_guard(manual_lifetime<T>& obj) noexcept
: ptr_(std::addressof(obj))
{}
// non-movable
destructor_guard(destructor_guard&&) = delete;
destructor_guard& operator=(destructor_guard&&) = delete;
~destructor_guard() noexcept(std::is_nothrow_destructible_v<T>) {
if (ptr_ != nullptr) {
ptr_->destroy();
}
}
void cancel() noexcept { ptr_ = nullptr; }
private:
manual_lifetime<T>* ptr_;
};
// Partial specialisation for types that don't need their destructors called.
template<typename T>
requires std::is_trivially_destrcutible_v<T>
struct destructor_guard<T> {
explicit destructor_guard(manual_lifetime<T>&) noexcept {}
void cancel() noexcept {}
};
// Class-template argument deduction to simplify usage
template<typename T>
destructor_guard(manual_lifetime<T>& obj) -> destructor_guard<T>;
通过这个工具,我们现在可以确保当异常抛出时协程状态中的变量被正确析构。
让我们对其他变量也使用这个类,确保当退出作用域时它们的析构函数也会被调用。
void __g_resume(__coroutine_state* s) {
auto* state = static_cast<__g_state*>(s);
std::coroutine_handle<void> coro_to_resume;
try {
switch (state->__suspend_point) {
case 0: goto suspend_point_0;
case 1: goto suspend_point_1; // <-- add new jump-table entry
default: std::unreachable();
}
suspend_point_0:
{
destructor_guard tmp1_dtor{state->__tmp1};
state->__tmp1.get().await_resume();
}
// int fx = co_await f(x);
{
state->__tmp2.construct_from([&] {
return f(state->x);
});
destructor_guard tmp2_dtor{state->__tmp2};
state->__tmp3.construct_from([&] {
return static_cast<task&&>(state->__tmp2.get()).operator co_await();
});
destructor_guard tmp3_dtor{state->__tmp3};
if (!state->__tmp3.get().await_ready()) {
state->__suspend_point = 1;
coro_to_resume = state->__tmp3.get().await_suspend(
std::coroutine_handle<__g_promise_t>::from_promise(state->__promise));
// A coroutine suspends without exiting scopes.
// So cancel the destructor-guards.
tmp3_dtor.cancel();
tmp2_dtor.cancel();
goto resume_coro;
}
// Don't exit the scope here.
//
// We can't 'goto' a label that enters the scope of a variable with a
// non-trivial destructor. So we have to exit the scope of the destructor
// guards here without calling the destructors and then recreate them after
// the `suspend_point_1` label.
tmp3_dtor.cancel();
tmp2_dtor.cancel();
}
suspend_point_1:
int fx = [&]() -> decltype(auto) {
destructor_guard tmp2_dtor{state->__tmp2};
destructor_guard tmp3_dtor{state->__tmp3};
return state->__tmp3.get().await_resume();
}();
// TODO: Implement
// co_return fx * fx;
} catch (...) {
state->__promise.unhandled_exception();
goto final_suspend;
}
final_suspend:
// TODO: Implement
// co_await promise.final_suspend();
resume_coro:
coro_to_resume.resume();
return;
}
现在我们的协程体会在任何异常出现的地方正确销毁局部变量,并且会在这些异常传播出协程体时正确调用 $promise$.$unhandled_-exception\left(\right)$。
要注意的是,如果 $promise$.$unhandled_-exception\left(\right)$ 方法自身因为异常退出 ( 例如重新抛出当前异常 ) 时,可能需要特殊处理。
在这种情况,协程需要捕获异常,标记为在最终挂起点挂起,然后重抛异常。
例如,__$g_-resume\left(\right)$ 函数的catch
块会像这样:
try {
// ...
} catch (...) {
try {
state->__promise.unhandled_exception();
} catch (...) {
state->__suspend_point = 2;
state->__resume = nullptr; // mark as final-suspend-point
throw;
}
}
然后我们需要给 __$g_-destroy$ 函数跳表增加额外条目:
switch (state->__suspend_point) {
case 0: goto suspend_point_0;
case 1: goto suspend_point_1;
case 2: goto destroy_state; // no variables in scope that need to be destroyed
// just destroy the coroutine-state object.
}
注意这个例子中,最终挂起点并不是必须跟 $co_-await$ $promise$.$final_-suspend\left(\right)$ 的挂起点一样。
这是因为 $promise$.$final_-suspend\left(\right)$ 挂起点通常有些额外的与 $co_-await$ 表达式相关的临时对象,这些对象需要在 $coroutine_-handle$::$destroy\left(\right)$ 调用时销毁。而在这里,如果 $promise$.$unhandled_-exception\left(\right)$ 因为异常退出,这些对象不再存活,所以不需要被 $coroutine_-handle$::$destroy\left(\right)$ 销毁。
第十一步:实现 co_return
下一步是实现 $co_-return$ $fx$ $*$ $fx;$ 语句。
与前面相比,这一步相对简单。
$co_-return$ <$expr$> 语句会映射成:
promise.return_value(<expr>);
goto final-suspend-point;
所以我们可以简单地把 TODO 注释替换成:
state->__promise.return_value(fx * fx);
goto final_suspend;
简单。
第十二步:实现 final_suspend()
代码的最后一个 TODO 是实现 $co_-await$ $promise$.$final_-suspend\left(\right)$ 语句。
$final_-suspend\left(\right)$ 方法返回一个临时的 $task$::$promise_-type$::$final_-awaiter$ 类型,后者存储在协程状态中,并在 __$g_-destroy$ 内销毁。
这个类型没有重载 $operator$ $co_-await\left(\right)$,所以我们不需要额外的临时对象存储调用结果。
就像 $task$::$awaiter$ 类型,它也通过 $await_-suspend\left(\right)$ 返回协程句柄。所以我们需要确保对返回的句柄调用 $resume\left(\right)$。
如果协程没有在最终挂起点挂起,那么协程状态会被隐式销毁。所以我们需要在执行到协程结束时,删除状态对象。
而且,因为所有的最终挂起逻辑需要是noexcept
的,我们不需要担心这里的子表达式会抛出异常。
我们先给 __$g_-state$ 类型加上数据成员。
struct __g_state : __coroutine_state_with_promise<__g_promise_t> {
__g_state(int&& __x);
~__g_state();
int __suspend_point = 0;
int x;
manual_lifetime<std::suspend_always> __tmp1;
manual_lifetime<task> __tmp2;
manual_lifetime<task::awaiter> __tmp3;
manual_lifetime<task::promise_type::final_awaiter> __tmp4; // <---
};
然后我们可以像下面这样实现最终挂起表达式体:
final_suspend:
// co_await promise.final_suspend
{
state->__tmp4.construct_from([&]() noexcept {
return state->__promise.final_suspend();
});
destructor_guard tmp4_dtor{state->__tmp4};
if (!state->__tmp4.get().await_ready()) {
state->__suspend_point = 2;
state->__resume = nullptr; // mark as final suspend-point
coro_to_resume = state->__tmp4.get().await_suspend(
std::coroutine_handle<__g_promise_t>::from_promise(state->__promise));
tmp4_dtor.cancel();
goto resume_coro;
}
state->__tmp4.get().await_resume();
}
// Destroy coroutine-state if execution flows off end of coroutine
delete state;
return;
接着我们需要更新 __$g_-destroy$ 函数来处理新的挂起点。
void __g_destroy(__coroutine_state* state) {
auto* state = static_cast<__g_state*>(s);
switch (state->__suspend_point) {
case 0: goto suspend_point_0;
case 1: goto suspend_point_1;
case 2: goto suspend_point_2;
default: std::unreachable();
}
suspend_point_0:
state->__tmp1.destroy();
goto destroy_state;
suspend_point_1:
state->__tmp3.destroy();
state->__tmp2.destroy();
goto destroy_state;
suspend_point_2:
state->__tmp4.destroy();
goto destroy_state;
destroy_state:
delete state;
}
现在我们有了一个完整的 $g\left(\right)$ 协程函数转换结果。
结束了!
还是……
第十三步:实现对称转移和无操作协程
我们上面实现的 __$g_-resume\left(\right)$ 函数方式有个问题。
之前的文章中有详细讨论过,所以如果你想了解更多,可以查看C++协程(4):理解对称转移。
expr.await 规范给了一点关于我们应该怎么处理返回协程句柄的 $await_-suspend$ 的线索:
如果 $await_-suspend$ 类型时 $std$::$coroutine_-handle$<$Z$>,会调用 $.resume\left(\right)$。
备注1:这会恢复 $await_-suspend$ 返回的协程。可以用这种方式连续恢复任意数量的协程,最终控制流会返回给现在协程的调用者/恢复者。
这个备注,虽然不是规范,也不是约束,但是十分鼓励编译器以尾调用方式实现恢复协程,而不是递归方式。因为在协程循环恢复对方的过程中,递归方式很容易导致栈无限制增长。
问题原因是我们是在 __$g_-resume\left(\right)$ 函数体内调用下一个协程的 $.resume\left(\right)$ 然后返回,因此 __$g_-resume\left(\right)$ 的栈帧会在下一个协程挂起返回后才会释放。
编译器可以把恢复下一个协程优化为尾调用。以这种方式,编译器生成的代码会先弹出当前栈帧,保留返回地址,然后执行 $jmp$ 指令跳转到下一个协程的恢复函数。
因为C++
并没有机制让尾部位置的函数调用变成尾调用,我们需要从恢复函数中返回来释放栈空间,然后让调用者恢复下一个协程。
下一个协程也可能需要在挂起时恢复另一个协程,而且这种恢复可能会无限下去,所以调用者需要在循环中恢复协程。
这种循环通常叫做蹦床循环 ( $trampoline$ $loop$ ),因为我们从一个协程回到循环然后再跳到下一个协程。
如果我们把恢复函数的签名改成返回指向下个协程状态的指针,而不是 $void$,那么 $coroutine_-handle$::$resume\left(\right)$ 函数可以立即调用下个协程的 __$resume\left(\right)$ 指针来恢复它。
我们改一下 __$coroutine_-state$ 的 __$resume_-fn$ 的签名:
struct __coroutine_state {
using __resume_fn = __coroutine_state* (__coroutine_state*);
using __destroy_fn = void (__coroutine_state*);
__resume_fn* __resume;
__destroy_fn* __destroy;
};
然后我们可以像这样编写 $coroutine_-handle$::$resume\left(\right)$:
void std::coroutine_handle<void>::resume() const {
__coroutine_state* s = state_;
do {
s = s->__resume(s);
} while (/*some condition*/);
}
下个问题是:“循环条件是什么?”
这时就轮到 $std$::$noop_-coroutine\left(\right)$ 帮忙了。
$std$::$noop_-coroutine$ 是一个工厂函数,返回一个特殊协程句柄,具有无操作的 $resume\left(\right)$ 和 $destroy\left(\right)$ 方法。如果一个协程挂起,并且 $await_-suspend\left(\right)$ 返回无操作句柄,意味着没有协程需要恢复,它的 $coroutine_-handle$::$resume\left(\right)$ 会返回给调用者。
所以我们需要实现 $std$::$noop_-coroutine\left(\right)$ 以及 $coroutine_-handle$::$resume\left(\right)$ 条件,让 __$coroutine_-state$ 指针指向无操作协程状态时变为 $false$ 并退出循环。
一种策略是定义一个静态 __$coroutine_-state$ 变量,作为无操作协程状态。$std$::$noop_-coroutine\left(\right)$ 函数可以返回一个指向该对象的协程句柄,然后我们可以比较 __$coroutine_-state$ 指针和那个对象的地址,判断协程句柄是否为无操作协程。
首先我们定义这个特殊的无操作协程状态对象:
struct __coroutine_state {
using __resume_fn = __coroutine_state* (__coroutine_state*);
using __destroy_fn = void (__coroutine_state*);
__resume_fn* __resume;
__destroy_fn* __destroy;
static __coroutine_state* __noop_resume(__coroutine_state* state) noexcept {
return state;
}
static void __noop_destroy(__coroutine_state*) noexcept {}
static const __coroutine_state __noop_coroutine;
};
inline const __coroutine_state __coroutine_state::__noop_coroutine{
&__coroutine_state::__noop_resume,
&__coroutine_state::__noop_destroy,
};
然后我们可以特例化实现 $std$::$coroutine_-handle$<$noop_-coroutine_-promise$>。
namespace std
{
struct noop_coroutine_promise {};
using noop_coroutine_handle = coroutine_handle<noop_coroutine_promise>;
noop_coroutine_handle noop_coroutine() noexcept;
template<>
class coroutine_handle<noop_coroutine_promise> {
public:
constexpr coroutine_handle(const coroutine_handle&) noexcept = default;
constexpr coroutine_handle& operator=*(const coroutine_handle&) noexcept = default;
constexpr explicit operator bool() noexcept { return true; }
constexpr friend bool operator==(coroutine_handle, coroutine_handle) noexcept {
return true;
}
operator coroutine_handle<void>() const noexcept {
return coroutine_handle<void>::from_address(address());
}
noop_coroutine_promise& promise() const noexcept {
static noop_coroutine_promise promise;
return promise;
}
constexpr void resume() const noexcept {}
constexpr void destroy() const noexcept {}
constexpr bool done() const noexcept { return false; }
constexpr void* address() const noexcept {
return const_cast<__coroutine_state*>(&__coroutine_state::__noop_coroutine);
}
private:
constexpr coroutine_handle() noexcept = default;
friend noop_coroutine_handle noop_coroutine() noexcept {
return {};
}
};
}
然后我们可以更新 $coroutine_-handle$::$resume\left(\right)$,在返回无操作协程状态时退出。
void std::coroutine_handle<void>::resume() const {
__coroutine_state* s = state_;
do {
s = s->__resume(s);
} while (s != &__coroutine_state::__noop_coroutine);
}
最后,我们可以更新 __$g_-resume\left(\right)$ 返回 __$coroutine_-state*$。
这部分只涉及更新签名并替换:
coro_to_resume = ...;
goto resume_coro;
以及
auto h = ...;
return static_cast<__coroutine_state*>(h.address());
并在函数的最后面 ( $delete$ $state;$ 语句之后 ) 添加:
return static_cast<__coroutine_state*>(std::noop_coroutine().address());
最后一件事
细心的人可能发现了,协程状态类型 __$g_-state$ 要比需要的大。
$4$ 个存储临时值的数据成员分别为它们的值保留了空间。然而,一些临时值的生命周期并不重叠,所以理论上可以通过在对象生命周期结束后,给下一个对象重复使用空间的方式,节省空间。
为了利用这点,我们可以把数据成员定义在一个合适的匿名union
中。
看一下我们现在的临时变量生命周期:
- __$tmp1$ - 只在 $co_-await$ $promise$.$initial_-suspend\left(\right);$ 语句存活
- __$tmp2$ - 只在 $int$ $fx$ $=$ $co_-await$ $f\left(x\right);$ 语句存活
- __$tmp3$ - 只在 $int$ $fx$ $=$ $co_-await$ $f\left(x\right);$ 语句存活 - 生命周期内嵌于 __$tmp2$
- __$tmp4$ - 只在 $co_-await$ $promise$.$final_-suspend\left(\right);$ 语句存活
因为 __$tmp2$ 和 __$tmp3$ 的生命周期重启,我们必须把它们一起放在同一个struct
,保证同时间它们都存活。
然而,__$tmp1$ 和 __$tmp4$ 成员生命周期不重叠,所以可以一起放在匿名union
中。
因此,我们可以把数据成员定义改成:
struct __g_state : __coroutine_state_with_promise<__g_promise_t> {
__g_state(int&& x);
~__g_state();
int __suspend_point = 0;
int x;
struct __scope1 {
manual_lifetime<task> __tmp2;
manula_lifetime<task::awaiter> __tmp3;
};
union {
manul_lifetime<std::suspend_always> __tmp1;
__scope1 __s1;
manual_lifetime<task::promise_type::final_awaiter> __tmp4;
};
};
然后,因为 __$tmp2$ 和 __$tmp3$ 变量都内嵌在 __$s1$ 对象,我们需要把它们的引用改成例如 $state$->__$s1$.$tmp2$ 的方式。不过其他代码不用变。
这样协程状态可以节省 $16$ 字节,因为 __$tmp1$ 和 __$tmp4$ 数据成员不再需要额外的空间对齐,它们会被对齐到指针大小,即使是空类型。
放到一起
好的,让我们看看下面协程函数生成的代码:
task g(int x) {
int fx = co_await f(x);
co_return fx * fx;
}
是下面这样:
/////
// The coroutine promise-type
using __promise_t = std::coroutine_traits<task, int>::promise_type;
__coroutine_state* __g_resume(__coroutine_state* s);
void __g_destroy(__coroutine_state* s);
/////
// The coroutine-state definition
struct __g_state : __coroutine_state_with_promise<__g_promise_t> {
__g_state(int&& x)
: x(static_cast<int&&>(x)) {
// Initialise the function-pointers used by coroutine-handle methods.
this->__resume = &__g_resume;
this->__destroy = &__g_destroy;
// Used placement-new to initialise the promise object in the base-class
// after we've intialised the argument copies.
::new ((void*)std::addressof(this->__promise))
__g_promise_t(construct_promise<__g_promise_t>(this->x));
}
~__g_state() {
this->__promise.~__g_promise_t();
}
int __suspend_point = 0;
// Argument copies
int x;
// Local variables/temporaries
struct __scope1 {
manual_lifetime<task> __tmp2;
manual_lifetime<task::awaiter> __tmp3;
};
union {
manual_lifetime<std::suspend_always> __tmp1;
__scope __s1;
manual_lifetime<task::promise_type::final_awaiter> __tmp4;
};
};
/////
// The "ramp" function
task g(int x) {
std::unique_ptr<__g_state> state(new __g_state(static_cast<int&&>(x)));
decltype(auto) return_value = state->__promise.get_return_object();
state->__tmp1.construct_from([&]() -> decltype(auto) {
return state->__promise.initial_suspend();
});
if (!state->__tmp1.get().await_ready()) {
state->__tmp1.get().await_suspend(
std::coroutine_handle<__g_promise_t>::from_promise(state->__promise));
state.release();
// fall through to return statement below.
} else {
// Coroutine did not suspend. Start excuting the body immediately.
__g_resume(state.release());
}
return return_value;
}
/////
// The "resume" function
__coroutine_state* __g_resume(__coroutine_state* s) {
auto* state = static_cast<__g_state*>(s);
try {
switch (state->__suspend_point) {
case 0: goto suspend_point_0;
case 1: goto suspend_point_1; // <-- add new jump-table entry
default: std::unreachable();
}
suspend_point_0:
{
destructor_guard tmp1_dtor{state->__tmp1};
state->__tmp1.get().await_resume();
}
// int fx = co_await f(x);
{
state->__s1.__tmp2.construct_from([&] {
return f(state->x);
});
destructor_guard tmp2_dtor{state->__s1.__tmp2};
state->__s1.__tmp3.construct_from([&] {
return static_cast<task&&>(state->__s1.__tmp2.get()).operator co_await();
});
destructor_guard tmp3_dtor{state->__s1.__tmp3};
if (!state->__s1.__tmp3.get().await_ready()) {
state->__suspend_point = 1;
auto h = state->__s1.__tmp3.get().await_suspend(
std::coroutine_handle<__g_promise_t>::from_promise(state->__promise));
// A coroutine suspends without exiting scopes.
// So cancel the destructor-guards.
tmp3_dtor.cancel();
tmp2_dtor.cancel();
return static_cast<__coroutine_state*>(h.address());
}
// Don't exit the scope here.
// We can't 'goto' a label that enters the scope of a variable with a
// non-trivial destructor. So we have to exit the scope of the destructor
// guards here without calling the destructors and then recreate them after
// the `suspend_point_1` label.
tmp3_dtor.cancel();
tmp2_dtor.cancel();
}
suspend_point_1:
int fx = [&]() -> decltype(auto) {
destructor_guard tmp2_dtor{state->__s1.__tmp2};
destructor_guard tmp3_dtor{state->__s1.__tmp3};
return state->__s1.__tmp3.get().await_resume();
}();
// co_return fx * fx;
state->__promise.return_value(fx * fx);
goto final_suspend;
} catch (...) {
state->__promise.unhandled_exception();
goto final_suspend;
}
final_suspend:
// co_await promise.final_suspend
{
state->__tmp4.construct_from([&]() noexcept {
return state->__promise.final_suspend();
});
destructor_guard tmp4_dtor{state->__tmp4};
if (!state->__tmp4.get().await_ready()) {
state->__suspend_point = 2;
state->__resume = nullptr; // mark as final suspend-point
auto h = state->__tmp4.get().await_suspend(
std::coroutine_handle<__g_promise_t>::from_promise(state->__promise));
tmp4_dtor.cancel();
return static_cast<__coroutine_state*>(h.address());
}
state->__tmp4.get().await_resume();
}
// Destroy coroutine-state if execution flows off end of coroutine
delete state;
return static_cast<__coroutine_state*>(std::noop_coroutine().address());
}
/////
// The "destroy" function
void __g_destroy(__coroutine_state* s) {
auto* state = static_cast<__g_state*>(s);
switch (state->__suspend_point) {
case 0: goto suspend_point_0;
case 1: goto suspend_point_1;
case 2: goto suspend_point_2;
default: std::unreachable();
}
suspend_point_0:
state->__tmp1.destroy();
goto destroy_state;
suspend_point_1:
state->__s1.__tmp3.destroy();
state->__s1.__tmp2.destroy();
goto destroy_state;
suspend_point_2:
state->__tmp4.destroy();
goto destroy_state;
destroy_state:
delete state;
}
最终代码的完全可编译版本可以看https://godbolt.org/z/xaj3Yxabn。
有关C++
协程机制的 $5$ 部分系列文章到此结束。
这些信息可能比你想要了解的还要多,希望它能帮助你理解和揭开协程的神秘面纱。
感谢您坚持到最后!
下次再见,Lewis.