C++协程(2):理解co_await
在之前的文章中,我介绍了函数和协程在高级表现的区别,但还没有讲到到C++
协程标准的语法和语义。
C++
协程标准提供的一个关键能力是暂停协程,并在之后恢复。这个机制是通过 $co_-await$ 提供的。
在揭开协程的神秘面纱前,我们需要理解 $co_-await$ 的工作方式,了解它是如何暂停和恢复协程的。在这篇文章中,我会解释 $co_-await$ 的机制,并介绍Awaitable与Awaiter的概念。
在这之前,作为背景,我想先简单回顾下协程标准。
协程标准带来了什么?
- 三个关键字 $co_-await$ ,$co_-yield$ 和 $co_-return$
- 一些 $std$::$experimental$ 命名空间的新类型:
- $coroutine_-handle$<$P$>
- $coroutine_-traits$<$Ts$…>
- $suspend_-always$
- $suspend_-never$
- 一种允许库开发者与协程交互、客制化行为的机制
- 一个让异步代码更容易编写的语言能力
C++
协程标准可以认为在语言层面提供了一个低级汇编语言协程。这些功能很难以一种安全的方式直接使用,主要是提供给库开发者来实现高级抽象,从而应用开发者可以安全使用。
之后的语言标准 ( 希望是C++20
) 计划加入这些低级功能,同时让标准库提供一些包装了这些功能的高级类型,让应用开发者以更方便安全的方式使用。
编译器 <-> 库交互
有趣的是,协程标准并没有明确定义协程语义,包括协程如何把值返回给主调,$co_-return$ 如何处理返回值,如何处理传播到协程之外的异常,以及协程应该在哪个线程恢复。
相反的,它指定了一种可以让库代码客制化协程行为的机制,通过实现特定的接口类型。编译器使用库代码生成对应的调用。这种方式很像range
模式,后者可以让库开发者通过定义 $begin\left(\right)$ / $end\left(\right)$ 和 $iterator$ 类型,来客制化代码。
协程的这种没有给机制定义特殊语义的方式,让它变成了一个强大的工具,允许库开发者实现各种类型的协程,以各种方式,出于各种目的。
例如,你可以实现一个异步生成单个值的协程,或者一个 $Lazy$ 生成值序列的协程,或者一个遇到 $nullopt$ 就提前退出的,简化 $optional$<$T$> 值的协程。
协程标准定义了两个接口:$Promise$ 和 $Awaitable$ 。
$Promise$ 接口指定了协程客制化行为的方法。库开发者可以通过该接口,实现当协程被调用时的行为,协程返回时的行为 ( 包括正常返回和抛出未处理异常 ),协程内使用 $co_-await$ 或者 $co_-yield$ 的行为。
$Awaitable$ 接口指定控制 $co_-await$ 语义的方法。当一个值是可以 $co_-await$ 的,代码就会被翻译成一系列的对 $awaitable$ 对象方法的调用。这个对象可以决定是否暂停当前协程,在暂停前执行一些逻辑用于后续的恢复,以及在恢复后生成 $co_-await$ 表达式的值。
我会在之后的文章再介绍 $Promise$ ,现在先来看看 $Awaitable$ 接口。
Awaiter和 Awatiable:解释co_await
$co_-await$ 运算符是一个新的一元运算符,接收一个值,比如:$co_-await$ $someValue$ 。
$co_-await$ 运算符只能在协程上下文中使用。这有一点重言了,因为根据定义,包含 $co_-await$ 操作符的函数都应该被编译成协程。
支持 $co_-await$ 运算符的类型被叫做 $Awaitable$ 类型。
注意,$co_-await$ 运算符能否对一个类型使用,取决于 $co_-await$ 出现的协程上下文。协程使用的 $promise$ 类型可以通过 $await_-transform$ 方法 ( 之后介绍 ),决定 $co_-await$ 的语义。
为了更精确,我喜欢使用术语一般 Awaitable ( $Normally$ $Awaitable$ ) 来描述一个协程上下文的 $promise$ 类型没有 $await_-transform$ 方法且支持 $co_-await$ 的协程。使用术语上下文 Awaitable来描述一个协程上下文的 $promise$ 类型存在 $await_-transform$ 方法的协程,这种类型仅在某些特别的上下文中才支持 $co_-await$ 运算符。
一个Awaiter类型实现了 $co_-await$ 调用会时使用到的三个方法:$await_-ready$ ,$await_-suspend$ 和 $await_-resume$ 。
注意我不要脸地从C#
asnyc
关键字中“借了” $Awaiter$ 这个术语。C#
中这个术语指代可以通过 $GetAwaiter\left(\right)$ 方法返回类似于C++
$Awaiter$ 对象的类型,而C#
的 $Awaiter$ 也与C++
有着诡异的相似,具体可以看这篇文章。
注意一个类型既可以是 $Awaitable$ 类型,也可以是 $Awaiter$ 类型。
当编译器看到 $co_-awaiter$<$expr$> 表达式时,根据类型的不同,可能会有多种行为。
获取 Awaiter
编译器要做的第一件事就是生成从挂起对象上获取 $Awaiter$ 对象的代码。$N4680$ 在 $5.3.8\left(3\right)$ 小节中列出了一系列的获取 $awaiter$ 的步骤。
假设挂起协程的 $promise$ 类型是 $P$ ,并且 $promise$ 在当前协程中是一个左值引用。
如果 $promise$ 类型 $P$ ,存在 $await_-transform$ 方法,<$expr$> 会被传入 $promise$.$await_-transform$(<$expr$>) 调用来获取 $Awaitable$ 值,记为 $awaitable$ 。相反,如果 $promise$ 类型没有 $await_-transform$ 成员,我们会直接使用 <$expr$> 来作为 $Awaitable$ 对象,同样记为 $awaitable$ 。
然后,如果 $Awaitable$ 对象 $awaitable$ 重载了 $operator$ $co_-awaiter\left(\right)$ ,那么这个函数就会被调用,来获取 $Awaiter$ 对象。否则,$awaitable$ 会被直接作为 $awaiter$ 对象使用。
如果我们把这些规则使用函数 $get_-awaitable\left(\right)$ 和 $get_-awaiter\left(\right)$ 来编码,看起来就是这样:
template <typename P, typename T>
decltype(auto) get_awaitable(P& promise, T&& expr)
{
if constexpr (has_any_await_transform_member_v<P>)
return promise.await_transform(static_cast<T&&>(expr));
else
return static_cast<T&&>(expr);
}
template <typename Awaitable>
decltype(auto) get_awaiter(Awaitable&& awaitable)
{
if constexpr (has_member_operator_co_await_v<Awaitable>)
return static_cast<Awaitable&&>(awaitable).operator co_await();
else if constexpr (has_non_member_operator_co_await_v<Awaitable&&>)
return operator co_await(static_cast<Awaitable&&>(awaitable));
else
return static_cast<Awaitable&&>(awaitable);
}
挂起 Awaiter
假设我们把逻辑封装成上面那种获取 $Awaiter$ 对象的函数,那么 $co_-await$<$expr$> 可以简单翻译成:
{
auto&& value = <expr>;
auto&& awaitable = get_awaitable(promise, static_cast<decltype(value)>(value));
auto&& awaiter = get_awaiter(static_cast<decltype(awaitable)>)(awaitable);
if (!awaiter.await_ready())
{
using handle_t = std::experimental::coroutine_handle<P>;
using await_suspend_result_t =
decltype(awaiter.await_suspend(handle_t::from_promise(p)));
<suspend-coroutine>
if constexpr (std::is_void_v<await_suspend_result_t>)
{
awaiter.await_suspend(handle_t::from_promise(p));
<return-to-caller-or-resumer>
}
else
{
static_assert(
std::is_same_v<await_suspend_result_t, bool>,
"await_suspend() must reutrn 'void' or 'bool'.");
if (awaiter.await_suspend(handle_t::from_promise(p)))
{
<return-to-caller-or-resumer>
}
}
<resume-point>
}
return awaiter.await_resume();
}
void
版本的 $await_-suspend\left(\right)$ 无条件地在调用返回后交还控制权,而bool
版本的则允许 $awaiter$ 对象有条件地在返回后恢复协程执行。
bool
版本的 $await_-suspend\left(\right)$ 在 $awaiter$ 发起一个需要异步完成的操作时十分有用。在这种情况下,当任务异步完成后,$await_-suspend$ 可以返回false
,表示协程应该立即恢复并继续执行。
在 <$suspend$-$coroutine$> ( 暂停协程 ) 处,编译器会生成一些代码来保存协程状态,并准备好被恢复。这些状态包括存储恢复点的位置,以及将一些寄存器值保存到当前协程帧内存中。
当前协程在 <$suspend$-$coroutine\left(\right)$> 操作完成后,就被认为已暂停。协程是在 $await_-suspend$ 调用内部被暂停的,一旦被暂停,就可以在之后被恢复或者销毁。
一旦 $await_-suspend\left(\right)$ 操作完成,就要在之后对这个协程进行恢复 ( 或者销毁 )。注意 $await_-suspend\left(\right)$ 返回false
相当于在当前线程中立即恢复协程。
$await_-ready\left(\right)$ 方法可以让你避免 <$suspend$-$coroutine$> 带来的开销,因为它可以返回操作是否可以在不需要暂停的前提下同步完成。
在 <$return$-$to$-$caller$-$or$-$resumer$> 点,控制权会返还给主调或者恢复者,并弹出协程调用栈帧,保留协程栈帧。
当 ( 或者 ) 执行到 <$resume$-$point$> ( 恢复点 ),暂停的协程将被恢复。例如,在通过 $await_-resume\left(\right)$ 方法获取结果之前。
$await_-resume\left(\right)$ 方法的返回值会作为 $co_-await$ 表达式的结果。$await_-resume\left(\right)$ 方法也可以抛出一个异常,并将其传播到 $co_-await$ 表达式之外。
注意如果一个异常传播到 $await_-suspend\left(\right)$ 调用之外,协程将在没有 $await_-resume\left(\right)$ 调用的情况下被自动恢复,并将异常传播到 $co_-await$ 表达式之外。
协程句柄
你可能注意到了,$coroutine_-handle$<$P$> 类型会被作为参数,在 $co_-await$ 表达式中传给 $await_-suspend\left(\right)$ 调用。
这个类型代表一个无主的协程帧句柄,可以用来恢复协程执行,或者销毁协程帧。它可以用来获取协程的 $promise$ 对象。
$coroutine_-handle$ 类型有着如下 (省略了的) 接口:
namespace std::experimental
{
template <typename Promise>
struct coroutine_handle;
template <>
struct coroutine_handle<void>
{
bool done() const;
void resume();
void destroy();
void* address() const;
static coroutine_handle from_address(void* address);
};
template <typename Promise>
struct coroutine_handle : coroutine_handle<void>
{
Promise& promise() const;
static coroutine_handle from_promise(Promise& promise);
static coroutine_handle from_address(void* address);
};
}
实现一个 $Awaitable$ 类型的时候,与 $coroutine_-handle$ 相关的关键方法是 $.resume\left(\right)$,它会在在操作完成并且想要恢复挂起中的协程执行的时候被调用。对 $coroutine_-handle$ 调用 $.resume\left(\right)$ 会在 <$resume$-$point$> 重新激活一个暂停的协程,在到达下一个 <$return$-$to$-$caller$-$or$-$resumer$> 点处返回。
$.destroy\left(\right)$ 方法摧毁协程帧,调用作用域内的变量析构器,释放协程帧使用的内存空间。一般情况下,你不需要主动 ( 事实上真的需要避免 ) 调用 $.destroy\left(\right)$,除非你是一个正在实现协程 $promise$ 类型的库开发者。通常,协程帧会被某些协程返回的RAII
类型所持有。所以再调用 $.destroy\left(\right)$ 可能导致一个双重析构的bug。
$.promise\left(\right)$ 方法返回一个协程 $promise$ 对象的引用。然后,就像 $.destroy\left(\right)$,一般只会在需要编写 $promise$ 类型时有用。你应该把 $promise$ 对象视为协程的一个内部细节实现。对于大部分一般 $Awaitable$ 类型,你应该使用 $coroutine_-handle$<$void$> 而不是 $coroutine_-handle$<$Promise$> 作为 $await_-suspend\left(\right)$ 方法的参数。
$coroutine_-handle$<$P$>::$from_-promise$($P$& $promise$) 函数允许从协程 $promise$ 对象引用中重新构造协程。注意你必须保证类型 $P$ 与某个正在使用的协程帧匹配,如果 $P$ 的类型继承了 $promise$ 类型,并用于构造 $coroutine_-handle$<$Base$> ,会导致一些未定义行为。
$.address\left(\right)$ / $from_-address\left(\right)$ 函数允许把一个协程句柄转换成 $void*$ 指针,或者从一个 $void*$ 指针转换协程句柄。
不需要同步的异步代码
$co_-await$ 的一个十分有用的设计是协程可以在被暂停后,把控制权移交给主调 / 恢复者前执行代码。
这可以让 $Awaiter$ 对象在被暂停后发起一个异步操作,把被暂停协程的 $coroutine_-handle$ 传给该操作,并在不需要任何额外同步的前提下安全地恢复操作 ( 可能在其他线程上 )。
例如,在 $await_-suspend\left(\right)$ 调用中,发起一个异步读操作,当协程被暂停,意味着我们可以在读操作完成后,恢复协程的执行。这一步不需要任何线程同步操作,不需要协调发起线程和执行线程的关系。
Time Thread 1 Thread 2
| -------- --------
| .... Call OS - Wait for I/O event
| Call await_ready() |
| <supend-point> |
| Call await_suspend(handle) |
| Store handle in operation |
V Start AsyncFileRead ---+ V
+-----> <AsyncFileRead Completion Event>
Load coroutine_handle from operation
Call handle.resume()
<resume-point>
Call to await_resume()
execution continues....
Call to AsyncFileRead returns
Call to await_suspend() returns
<return-to-caller/resumer>
当使用这种方便的方式时,你需要小心的一点是,当把协程句柄发给其他线程时,其他线程可能在 $await_-suspend\left(\right)$ 调用返回前就尝试恢复协程执行,导致在 $await_-suspend\left(\right)$ 未完成时,协程继续执行了。
当协程恢复后,做的第一件事是调用 $await_-resume\left(\right)$ 获取结果,然后通常立马析构 $Awaiter$ 对象 ( 例如,$await_-suspend\left(\right)$ 调用的this
指针 )。然后协程可能执行直到完成,在 $await_-suspend\left(\right)$ 返回前销毁协程和 $promise$ 对象。
所以在 $await_-suspend\left(\right)$ 方法,一旦协程有可能被另一个线程并行地恢复,你就需要避免访问this
或者协程的 $.promise\left(\right)$ 对象,因为两者都可能被摧毁。总的来说,当暂停操作从发起到完成期间,只有局部变量是安全的。
与有栈协程相比
我想换个话题,快速地与现有的其他有栈协程比较下无栈协程在暂停后执行逻辑的能力,比如Win32 fibers
或者boost::context
。
对于许多有栈协程来说,暂停和恢复操作被合并成了“上下文切换”操作。通过“上下文切换”操作,当前协程在暂停后没有机会去执行逻辑,只是单纯地将控制权移交给其他协程。
这意味着,如果我们想要基于有栈协程实现一个类似于异步文件读取操作,我们需要在暂停协程之前发起操作。因此有可能操作会在线程被暂停之前就在其他线程上完成了,并等待恢复。这种潜在的其他线程完成操作和协程暂停之间的竞态,需要一些线程同步机制来选择并决定胜者。
一种可选的方式是使用内嵌上下文 ( $tranpoline$ $context$ ),它可以通过暂停发起操作的上下文来表示发起某个操作。然而,这可能需要额外的架构,和另外的上下文切换来保证正常使用,并且成本可能大于它试图避免的同步操作的成本。
减少内存分配
异步操作经常需要给每个操作分配状态,用于跟踪操作执行阶段。这些状态需要在操作执行过程中保留,直到操作完成才会被释放。
例如,调用异步Win32 I/O
函数需要你分配并传递一个 $OVERLAPPED$ 结构指针。主调需要保证指针在操作完成前都是有效的。
典型的基于回调的API
要求这些状态分配在堆上,并保证它们有着合适的生命周期。如果你正在执行很多操作,你可能需要给每个操作都分配和释放状态。如果这导致性能问题,你可能需要通过内存池来分配这些状态。
然而,当使用协程时,利用局部变量在协程帧内的特性,我们可以避免堆分配存储,因为协程帧内的变量在协程被暂停时也是存活的。
通过把 $co_-await$ 表达式中每个操作状态放到 $Awaiter$ 对象中,我们可以有效的从协程帧中“借用”内存。一旦操作完成,协程被恢复,$Awaiter$ 对象被摧毁,释放协程帧中其他局部变量使用的内存。
最终,协程帧还是分配在堆上。然而,只需要一次堆分配,协程帧就可以被用来执行许多异步操作。
你想一想,协程帧其实就是arena
内存分配器的一种高级表现。编译器计算出所有局部变量需要的arena
大小,然后零成本地分配所有局部变量!试着用传统的分配器来战胜它;)
例子:实现一个单线程同步原语
现在我们已经讲了许多 $co_-await$ 运算符的机制,我想实现一个基本的 $awaitable$ 同步原语:一个异步手动重置 $event$,来把这些知识运用到实践上。
这个 $event$ 的基本需求是对许多并行执行的协程来说是 $Awaitble$ 的,挂起操作会等暂停挂起协程,直到一个线程调用 $.set\left(\right)$ 方法,此时恢复所有挂起的协程。如果一个线程已经调用了 $.set\left(\right)$,协程应该在不暂停的前提下继续执行。
理想情况下,我想要让方法noexcept
,这需要避免堆分配,以及无锁实现。
2017/11/23 编辑:增加 async_manual_reset_event 的用例
用例看起来像是这样:
T value;
async_manual_reset_event event;
// A single call to produce a value
void producer()
{
value = some_long_running_computation();
// Publish the value by setting the event.
event.set();
}
// Supports multiple concurrent consumers
task<> consumer()
{
// Wait until the event is signalled by call to event.set()
// in the producer() function
co_await event;
// Now it's safe to consume 'value'
// This is guaranteed to 'happen after' assignment to 'value'
std::cout << value << std::endl;
}
让我们先想想这个 $event$ 可能的状态:$not$ $set$ 和 $set$ 。
当处于 $not$ $set$ 状态,存在一个等待 $set$ 状态挂起协程的列表 ( 可能为空 )。
当设置 $set$ 状态,就不应该存在挂起的协程,因为正在 $co_-await$ 这个 $event$ 的协程可以不暂停地继续执行。
这个状态可以通过一个 $std$::$atomic$<$void*$> 表示:
- 保存一个特殊指针,指向 $set$ 状态。我们使用 $event$ 的
this
指针来表示,因为它不可能跟列表中其他对象的地址相同。 - 否则,$event$ 处于 $not$ $set$ 状态,指针值是一个单独的挂起协程的链表头。
我们可以通过把状态存储在协程帧的 $awaiter$ 对象的方式,避免在堆上对节点进行额外分配。
让我们实现一个类似如下的类接口:
class async_manual_reset_event
{
public:
async_manual_reset_event(bool initiallySet = false) noexcept;
// No copying/moving
async_manual_reset_event(const asnyc_manual_reset_event&) = delete;
async_manual_reset_event(async_manual_reset_event&&) = delete;
async_manual_reset_event& operator=(const asnyc_manual_reset_event&) = delete;
async_manual_reset_event& operator=(async_manual_reset_event&&) = delete;
bool is_set() const noexcept;
struct awaiter;
awaiter operator co_await() const noexcept;
void set() noexcept;
void reset() noexcept;
private:
friend struct awaiter;
// - 'this' => set state
// - otherwise => not set, head of linked list of awaiter*.
mutable std::atomic<void*> m_state;
};
这里我们给出了一个相当简单直接的接口。主要要注意的点是 $operator$ $co_-await\left(\right)$ 返回一个未定义的 $awaiter$ 类型。
让我们先实现 $awaiter$ 。
实现 Awaiter
首先,需要知道正在等待哪个 $async_-manual_-reset_-event$ ,所以需要一个 $event$ 引用和一个初始化它的构造器。
它同样需要作为 $awaiter$ 链表的节点,所以也需要保存下一个 $awaiter$ 对象的指针。
它还需要保存正在执行 $co_-await$ 的挂起协程的 $coroutine_-handle$ ,用来在 $event$ 被 $set$ 之后恢复协程。我们不关心协程的 $promise$ 类型,所以只需要使用 $coroutine_-handle$<> ( $coroutine_-handle$<$void$> 的缩写 )。
最后,它需要实现 $Awaiter$ 接口,所以需要三个特殊方法 $await_-ready$ ,$await_-suspend$ 和 $await_-resume$ 。我们不需要 $co_-await$ 返回值,所以 $await_-resume$ 可以返回 $void$ 。
一旦我们把所有东西放到一起,基本的 $awaiter$ 接口看起来就像这样:
struct async_manual_reset_event::awaiter
{
awaiter(const async_manual_reset_event& event) noexcept
: m_event(event)
{}
bool await_ready() const noexcept;
bool await_suspend(std::experimental::coroutine_handle<> awaitinigCoroutine) noexcept;
void await_resume() noexcept {};
private:
friend class async_manual_reset_event;
const async_manual_reset_event& m_event;
std::experimental::coroutine_handle<> m_awaitingCoroutine;
awaiter* m_next;
};
当 $co_-await$ 一个 $event$ 时,我们不想让挂起协程在 $event$ 被 $set$ 的时候暂停。所以我们让 $await_-ready\left(\right)$ 在 $event$ 已经 $set$ 的时候返回true
。
bool async_manual_reset_event::awaiter::await_ready() const noexcept
{
return m_event.is_set();
}
接下来,让我们看看 $await_-suspend\left(\right)$ 方法,许多 $awaitable$ 类型的有趣行为都发生在这里。
首先它需要在 $m_-awaitingCoroutine$ 成员中保存协程句柄,用于后续调用 $.resume\left(\right)$ 。
保存句柄之后,需要把 $awaiter$ 自动地加入链表中。成功入队之后,如果 $event$ 还没有被设置成 $set$ 状态,返回true
表示我们不希望立即恢复协程,否则返回false
,表示协程应该立马恢复。
bool async_manual_reset_event::awaiter:await_suspend(
std::experimental::coroutine_handle<> awaitingCoroutine) noexcept
{
// Special m_state value that indicates the event is in the 'set' state.
const void* const setState = &m_event;
// Remember the handle of the awaiting coroutine.
m_awaitingCoroutine = awaitingCoroutine;
// Try to atomically push this awaiter onto the front of the list.
void* oldValue = m_event.m_state.load(std::memory_order_acquire);
do
{
// Resume immediately if already in 'set' state.
if (oldValue == setState) return false;
// Upload linked list to point at current head.
m_next = static_cast<awaiter*>(oldValue);
// Finally, try to swap the old list head, inserting this awaiter
// as the new list head.
} while (!m_event.m_state.compare_exchange_weak(
oldValue,
this,
std::memory_order_release,
std::memory_order_acquire));
// Successfully enqueued. Remain suspend.
return true;
}
注意我们使用 $acquire$ 内存序读取旧状态,这样我们就可以看到所有发生在 $set\left(\right)$ 调用之前的写操作了。
我们需要用 $release$ 内存序调用 $compare$-$exchange$ ,这样之后的 $set\left(\right)$ 就可以看到我们写入的 $m_-awaitingCoroutine$ 和更早写入的协程状态。
完成 event 类的剩余实现
我们已经定义好了 $awaiter$ 类型,再看回来 $async_-manual_-reset_-event$ 的方法。
首先是构造函数,它需要使用空的链表 ( 比如nullptr
) 初始化为 $not$ $set$ 状态,或者初始化为 $set$ 状态 ( 比如this
)。
async_manual_reset_event::async_manual_reset_event(
bool initiallySet) noexcept
: m_state(initiallySet ? this : nullptr)
{}
接下来,$is_-set\left(\right)$ 方法更直接,如果持有this
指针,那么就是 $set$ 状态。
bool async_manual_reset_event::is_set() const noexcept
{
return m_state.load(std::memory_order_acquire) == this;
}
接着是 $reset\left(\right)$ 方法,如果当前是 $set$ 状态,我们需要重置为空链表的 $not$ $set$ 状态,否则不做任何事:
void async_manual_reset_event::reset() noexcept
{
void* oldValue = this;
m_state.compare_exchange_strong(oldValue, nullptr, std::memory_order_acquire);
}
通过 $set\left(\right)$ 方法,我们可以用特殊的this
指针与当前值交换,从而设置状态为 $set$。如果存在挂起的协程,我们需要在返回前依次恢复。
void async_manual_reset_event::set() noexcept
{
// Needs to be 'release' so that subsequent 'co_await' has
// visibility of our prior writes.
// Needs to be 'acquire' so that we have visibility of prior
// writes by awaiting coroutines.
void* oldValue = m_state.exchange(this, std::memory_order_acq_rel);
if (oldValue != this)
{
// Wasn't already in 'set' state.
// Treat old value as head of a linked-list of waiters
// which we have now acquired and need to resume.
auto* waiters = static_cast<awaiter*>(oldValue);
while (waiters != nullptr)
{
// Read m_next before resuming the coroutines as resuming
// the coroutine will likely destroy the awaiter object.
auto* next = waiters->m_next;
waiters->m_awaitingCoroutine.resume();
waiters = next;
}
}
}
最后我们需要实现 $operator$ $co_-await\left(\right)$ 方法,只需要构造一个 $awaiter$ 对象。
async_manual_reset_event::awaiter
async_manual_reset_event::operator co_await() const noexcept
{
return awaiter{ *this };
}
这样我们就完成了一个无锁,无额外内存分配,noexcept
实现的 $awaitable$ 的异步手动-重置 $event$ 。
如果你想要试试这段代码,看看它在MSVC
和Clang
下的编译结果,可以看一下godbolt上的源码。
你也可以在cppcoro库 上看到这段实现,以及更多的其他的有用的 $awaitable$ 类型,例如 $async_-mutex$ 和 $async_-auto_-reset_-event$ 。
写在结尾
这篇文章介绍了 $operator$ $co_-await$ 是怎么通过 $Awaitable$ 和 $Awaiter$ 这两个concept
实现的。
同样也讲了怎么实现一个 $awaitable$ 的异步线程同步原语,使用了 $awaiter$ 对象在协程帧上分配的优点,避免了额外的堆分配。
我希望这篇文章可以帮助你理解新的 $co_-await$ 运算符。
在下一篇文章中,我将介绍 $Promise$ concept
,以及一个协程类型的开发者可以怎样设计协程的行为。
致谢
这段就不翻了吧~