[译]C++ 协程:理解 co_await 运算符

C++ 协程:理解 co_await 运算符

在之前关于 协程理论的博客 中,我介绍了一些函数和协程在较高层次上的一些不同,但没有详细介绍 C++ 协程技术规范(N4680)中描述的语法和语义。

协程技术规范中,C++ 新增的关键新功能是能够挂起协程,并能够在之后恢复。技术规范为此提供的机制是通过新的 co_await 运算符去实现。

理解 co_await 运算符的工作原理可以帮助我们揭开协程行为的神秘面纱,并了解它们如何被暂停和挂起的。在这篇文章中,我将解释 co_await 操作符的机制,并介绍 AwaitableAwaiter 类型的相关概念。

在深入讲解 co_await 之前,我想简要介绍一下协程的技术规范,以提供一些背景知识。

协程技术规范给我们提供了什么?

  • 三个新的关键字: co_awaitco_yieldco_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_awaitco_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_readyawait_suspendawait_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> 结果转换为 <

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值