协程的执行过程

声明

本文为本人原创,未经授权严禁转载。如需转载需要在文章最前面注明本文原始链接。

前言

C++ 协程是 C++20 引入的一种新特性,它允许函数在执行过程中挂起并在之后恢复执行。协程是无栈的,它们通过返回给调用者来挂起执行,而恢复执行所需的数据是与栈分开存储的。这使得协程可以用于顺序代码的异步执行(例如,处理非阻塞 I/O 而无需显式的回调),并且也支持对惰性计算的无限序列等算法的使用。

以下是 C++ 协程的一些关键用法和特性:

  1. 协程的定义

    • 协程函数可以通过包含 co_await 表达式来挂起执行直到被恢复。
    • co_yield 表达式用于挂起执行并返回一个值。
    • co_return 语句用于完成执行并返回一个值。
  2. 协程的返回类型

    • 每个协程都必须有一个满足特定要求的返回类型。
  3. 限制

    • 协程不能使用可变参数、普通返回语句或占位符返回类型(如 auto 或 Concept)。
    • consteval 函数、constexpr 函数、构造函数、析构函数和主函数不能是协程。
  4. 执行

    • 每个协程都与一个 promise 对象关联,该对象在协程内部被操作,用于提交结果或异常。
    • coroutine handle 用于在协程外部操作,以恢复协程的执行或销毁协程帧。
    • coroutine state 是内部的、动态分配的存储,包含 promise 对象、参数、当前挂起点的表示、局部变量和临时变量的生命周期跨越当前挂起点。
  5. 动态分配

    • 协程状态通过非数组 operator new 动态分配。
  6. Promise 类型

    • Promise 类型由编译器根据协程的返回类型使用 std::coroutine_traits 确定。
  7. co_await 表达式

    • co_await 一元操作符用于挂起协程,并将控制权返回给调用者。
  8. co_yield 表达式

    • co_yield 表达式用于返回一个值给调用者并挂起当前协程。

协程的创建

每个协程都与以下对象相关联:

当协程开始执行时,它执行以下步骤:

  1. 使用operator new分配协程状态对象。
  2. 将所有函数参数复制到协程状态对象中:按值传递的参数将被移动或复制,按引用传递的参数在协程状态中保持为引用(如果协程在引用对象生命周期结束后恢复,这可能会导致悬挂引用——见下文示例)。
  3. 调用承诺对象的构造函数。如果承诺类型的构造函数接受所有协程参数,则调用该构造函数;否则,调用默认构造函数。
  4. 调用promise.get_return_object()并保留返回结果在局部变量中。当协程首次挂起时,该结果将返回给调用者。在此步骤及之前抛出的任何异常将传播回调用者,而不是存储在承诺对象中。
  5. 调用promise.initial_suspend()并使用co_await等待其结果。典型的承诺类型会返回std::suspend_always(对于延迟启动的协程)或std::suspend_never(对于立即启动的协程)。
  6. co_await promise.initial_suspend()恢复执行时,开始执行协程的主体代码。
    截图 2024-05-28 20-00-03.png

co_await

co_await是 单目操作符,用于挂起协程的执行,并将控制权交回给调用者。它的操作数可以是以下两种类型之一的表达式:(1) 定义了成员 co_await 操作符的类类型,或者可以传递给非成员 co_await 操作符的类型;(2) 可以通过当前协程的 Promise::await_transform 方法转换为上述类类型的表达式。

co_await expr(协程等待表达式) co_await 表达式只能出现在常规函数体中的潜在求值表达式里(包括 lambda 表达式的函数体),并且不能出现在以下位置:

首先,expr 被转换成一个可等待对象(awaitable),转换过程如下:

然后,可以通过以下方法获取等待器对象(awaiter):

如果上述表达式是一个纯右值(prvalue),则等待器对象是从它实例化的临时对象。否则,如果表达式是一个引用左值(glvalue),则等待器对象就是它所引用的对象。

然后,调用 awaiter.await_ready()(这是一个避免挂起成本的快捷方式,如果已知结果已准备好或可以同步完成)。如果调用结果在上下文中转换为布尔值后为 false,则:

如果协程在 co_await 表达式中被挂起,并稍后恢复,则恢复点位于调用 awaiter.await_resume() 之前。

请注意,由于协程在进入 awaiter.await_suspend() 之前已完全挂起,因此该函数可以在没有额外同步的情况下自由地跨线程传递协程句柄。例如,它可以将句柄放入回调中,安排在线程池中的某个线程上执行异步 I/O 操作完成后的回调。在这种情况下,由于当前协程可能已经被恢复,并且执行了等待器对象的析构函数,因此在当前线程上 await_suspend() 继续执行的同时,所有并发操作 await_suspend() 应该将 *this 视为已销毁,并且在句柄发布到其他线程后不再访问它。

截图 2024-05-28 21-48-05.png

awaitable与awaiter

awaitableawaiter 是 C++20 协程特性中的两个相关但不同的概念:

  1. Awaitable

    • awaitable 是一个可以被 co_await 表达式等待的对象。
    • 它是一个类型,这个类型的对象能够表达等待的概念,并且知道如何与 co_await 表达式协作。
    • awaitable 类型必须提供 operator co_await,这个操作符返回一个 awaiter 对象。
    • awaitable 类型可以是任何类型,只要它或者通过成员函数或者通过转换操作能够提供与 co_await 表达式协作的机制。
  2. Awaiter

    • awaiter 是由 awaitable 类型的 operator co_await 返回的对象。
    • awaiter 对象负责管理实际的等待逻辑,包括检查操作是否就绪、挂起协程的执行、以及在操作完成时恢复协程。
    • awaiter 类型通常会提供三个成员函数:await_readycoroutine_handle<>) 和 await_resume(
    • await_ready() 用来检查异步操作是否已经就绪,从而可能避免不必要的挂起。
    • await_suspend(handle) 在异步操作未就绪时被调用,用来挂起协程的执行。它接收一个 std::coroutine_handle<> 参数,代表当前协程的句柄。
    • await_resume() 在异步操作完成时被调用,用来恢复协程的执行,并返回操作的结果(如果异步操作有返回值)。

简单来说,awaitable 是一个定义了如何被等待的类型,而 awaiter 是一个包含实际等待逻辑的对象。当一个协程通过 co_await 表达式等待某个操作时,首先会从 awaitable 获取一个 awaiter 对象,然后由 awaiter 来管理挂起和恢复的细节。这种分离的设计使得 awaitable 可以专注于定义等待的概念,而 awaiter 则处理具体的挂起和恢复逻辑。

co_yield

co_yield表达式会将值返回给调用者并挂起当前协程,它等价于 co_await promise.yield_value(expr)promise.yield_value()会将值保存在promise中。

一般而言,promise.yield_valuesuspend_always{},这是一个特殊的awaiter, 它定义了空的await_suspend()await_resume()函数,且await_ready(始终返回false, 表示始终挂起。

co_return

  1. co_return 语句:当协程执行到 co_return 语句时,它将完成其执行流程。co_return 可以单独使用,也可以与一个表达式一起使用来返回一个值。

  2. 返回值处理

    • 如果使用 co_return;(没有表达式),则会调用协程的 promise 类的 return_void() 方法。
    • 如果使用 co_return expr;(表达式 expr 类型为 void),同样会调用 return_void()
    • 如果使用 co_return expr;(表达式 expr 类型非 void),则会调用 promise 类的 return_value(expr) 方法,并将表达式的值作为参数传递。promise会自动保存该值。
  3. 最终挂起点co_return 之后,会调用 promise 类的 final_suspend() 方法。final_suspend()也需要返回一个awaiter.

请 Ta 喝咖啡 ☕️