- 原文地址:C++ Coroutines: Understanding operator co_await
- 原文作者:lewissbaker
- 译文出自:掘金翻译计划
- 本文永久链接:github.com/xitu/gold-m…
- 译者:7Ethan
- 校对者:razertory,noahziheng
C++ 协程:理解 co_await
运算符
在之前关于 协程理论的博客 中,我介绍了一些函数和协程在较高层次上的一些不同,但没有详细介绍 C++ 协程技术规范(N4680)中描述的语法和语义。
协程技术规范中,C++ 新增的关键新功能是能够挂起协程,并能够在之后恢复。技术规范为此提供的机制是通过新的 co_await
运算符去实现。
理解 co_await
运算符的工作原理可以帮助我们揭开协程行为的神秘面纱,并了解它们如何被暂停和挂起的。在这篇文章中,我将解释 co_await
操作符的机制,并介绍 Awaitable 和 Awaiter 类型的相关概念。
在深入讲解 co_await
之前,我想简要介绍一下协程的技术规范,以提供一些背景知识。
协程技术规范给我们提供了什么?
- 三个新的关键字:
co_await
,co_yield
和co_return
std::experimental
命名空间的几个新类型:coroutine_handle<P>
coroutine_traits<Ts...>
suspend_always
suspend_never
- 一种能够让库的作者与协程交互并定制它们行为的通用机制。
- 一个使异步代码变得更加简单的语言工具!
C++ 协程技术规范在语言中提供的工具,可以理解为协程的低级汇编语言。 这些工具很难直接以安全的方式使用,主要是供库作者使用,用于构建应用程序开发人员可以安全使用的更高级别的抽象。
未来会将这些新的低级工具交付给即将到来的语言标准(可能是 C++20),以及标准库中伴随的一些高级类型,这些高级类型封装了这些低级构建块,应用程序开发人员将可以通过一种安全的方式轻松访问协程。
编译器与库的交互
有趣的是,协程技术规范实际上并没有定义协程的语义。它没有定义如何生成返回给调用者的值,没有定义如何处理传递给 co_return
语句的返回值,如何处理传递出协程的异常,它也没有定义应该恢复协程的线程。
相反,它指定了库代码的通用机制,那就是通过实现符合特定接口的类型来定制协程的行为。然后,编译器生成代码,在库提供的类型实例上调用方法。这种方法类似于库作者通过定义 begin()
/ end()
方法或 iterator
类型来定制基于范围的 for 循环的实现。
协程技术规范没有对协程的机制规定任何特定的语义,这使它成为一个强大的工具。它允许库作者为各种不同目的来定义许多不同种类的协程。
例如,你可以定义一个异步生成单个值的协程,或者一个延迟生成一系列值的协程,或者如果遇到 nullopt
值,则通过提前退出来简化控制流以消耗 optional <T>
值的协程。
协程技术规范定义了两种接口:Promise 接口和 Awaitable 接口。
Promise 接口指定用于自定义协程本身行为的方法。库作者能够自定义调用协程时发生的事件,如协程返回时(通过正常方式或通过未处理的异常返回),或者自定义协程中任何 co_await
或 co_yield
表达式的行为。
Awaitable 接口指定控制 co_await
表达式语义的方法。当一个值为 co_await
时,代码被转换为对 awaitable 对象上的方法的一系列调用。它可以指定:是否暂停当前协程,暂停调度协程以便稍后恢复后执行一些逻辑,还有在协程恢复后执行一些逻辑以产生 co_await
表达式的结果。
我将在以后的博客中介绍 Promise 接口的细节,现在我们先来看看 Awaitable 借口。
Awaiters 与 Awaitables:解释操作符 co_await
co_await
运算符是一个新的一元运算符,可以应用于一个值。例如:co_await someValue
。
co_await
运算符只能在协程的上下文中使用。这有点语义重复,因为根据定义,任何包含 co_await
运算符的函数体都将被编译为协程。
支持 co_await
运算符的类型称为 Awaitable 类型。
注意,co_await
运算符是否可以用作类型取决于 co_await
表达式出现的上下文。用于协程的 promise 类型可以通过其 await_transform
方法更改协程中的 co_await
表达式的含义(稍后将详细介绍)。
为了更具体地在需要的地方,我喜欢使用术语 Normally Awaitable 来描述在协程类型中没有 await_transform
成员的协程上下文中支持 co_await
运算符的类型。我喜欢使用术语 Contextually Awaitable 来描述一种类型,它在某些类型的协程的上下文中仅支持 co_await
运算符,因为协程的 promise 类型中存在 await_transform
方法。(我乐意接受这些名字的更好建议...)
Awaiter 类型是一种实现三个特殊方法的类型,它们被称为 co_await
表达式的一部分:await_ready
,await_suspend
和await_resume
。
请注意,我在 C# async
关键字的机制中“借用”了 “Awaiter” 这个术语,该机制是根据 GetAwaiter()
方法实现的,该方法返回一个对象,其接口与 c++ 的 Awaiter 概念惊人的相似。有关 C# awaiters 的更多详细信息,请参阅这篇博文。
请注意,类型可以是 Awaitable 类型和 Awaiter 类型。
当编译器遇到 co_await <expr>
表达式时,实际上可以根据所涉及的类型将其转换为许多可能的内容。
获取 Awaiter
编译器做的第一件事是生成代码,以获取等待值的 Awaiter 对象。在 N4680 章节 5.3.8(3) 中,有很多步骤可以获得 awaiter。
让我们假设等待协程的 promise 对象具有类型 P
,并且 promise
是对当前协程的 promise 对象的 l-value 引用。
如果 promise 类型 P
有一个名为 await_transform
的成员,那么 <expr>
首先被传递给 promise.await_transform(<expr>)
以获得 Awaitable 的值。 否则,如果 promise 类型没有 await_transform
成员,那么我们使用直接评估 <expr>
的结果作为 Awaitable 对象。
然后,如果 Awaitable 对象,有一个可用的运算符 co_await()
重载,那么调用它来获取 Awaiter 对象。 否则,awaitable
的对象被用作 awaiter 对象。
如果我们将这些规则编码到 get_awaitable()
和 get_awaiter()
函数中,它们可能看起来像这样:
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
因此,假设我们已经封装了将 <expr>
结果转换为 <