协程的执行过程
声明
本文为本人原创,未经授权严禁转载。如需转载需要在文章最前面注明本文原始链接。
前言
C++ 协程是 C++20 引入的一种新特性,它允许函数在执行过程中挂起并在之后恢复执行。协程是无栈的,它们通过返回给调用者来挂起执行,而恢复执行所需的数据是与栈分开存储的。这使得协程可以用于顺序代码的异步执行(例如,处理非阻塞 I/O 而无需显式的回调),并且也支持对惰性计算的无限序列等算法的使用。
以下是 C++ 协程的一些关键用法和特性:
-
协程的定义:
- 协程函数可以通过包含
co_await
表达式来挂起执行直到被恢复。 co_yield
表达式用于挂起执行并返回一个值。co_return
语句用于完成执行并返回一个值。
- 协程函数可以通过包含
-
协程的返回类型:
- 每个协程都必须有一个满足特定要求的返回类型。
-
限制:
- 协程不能使用可变参数、普通返回语句或占位符返回类型(如
auto
或 Concept)。 consteval
函数、constexpr
函数、构造函数、析构函数和主函数不能是协程。
- 协程不能使用可变参数、普通返回语句或占位符返回类型(如
-
执行:
- 每个协程都与一个
promise
对象关联,该对象在协程内部被操作,用于提交结果或异常。 coroutine handle
用于在协程外部操作,以恢复协程的执行或销毁协程帧。coroutine state
是内部的、动态分配的存储,包含 promise 对象、参数、当前挂起点的表示、局部变量和临时变量的生命周期跨越当前挂起点。
- 每个协程都与一个
-
动态分配:
- 协程状态通过非数组
operator new
动态分配。
- 协程状态通过非数组
-
Promise 类型:
Promise
类型由编译器根据协程的返回类型使用std::coroutine_traits
确定。
-
co_await
表达式:co_await
一元操作符用于挂起协程,并将控制权返回给调用者。
-
co_yield
表达式:co_yield
表达式用于返回一个值给调用者并挂起当前协程。
协程的创建
每个协程都与以下对象相关联:
- 承诺对象(promise object):这是在协程内部操作的对象。协程通过这个对象提交其结果或抛出的异常。承诺对象与
std::promise
类无关。 - 协程句柄:这是从协程外部操作的非拥有式句柄,用于恢复协程的执行或销毁协程框架。
- 协程状态:这是内部的、动态分配的存储对象(除非分配被优化掉),包含:
- 承诺对象:用于存储协程的结果或异常。
- 参数:所有参数都按值复制。
- 当前挂起点的表示:这样恢复操作知道从哪里继续执行,销毁操作知道哪些局部变量在作用域内。
- 局部变量和临时变量:其生命周期跨越当前挂起点。
当协程开始执行时,它执行以下步骤:
- 使用
operator new
分配协程状态对象。 - 将所有函数参数复制到协程状态对象中:按值传递的参数将被移动或复制,按引用传递的参数在协程状态中保持为引用(如果协程在引用对象生命周期结束后恢复,这可能会导致悬挂引用——见下文示例)。
- 调用承诺对象的构造函数。如果承诺类型的构造函数接受所有协程参数,则调用该构造函数;否则,调用默认构造函数。
- 调用
promise.get_return_object()
并保留返回结果在局部变量中。当协程首次挂起时,该结果将返回给调用者。在此步骤及之前抛出的任何异常将传播回调用者,而不是存储在承诺对象中。 - 调用
promise.initial_suspend()
并使用co_await
等待其结果。典型的承诺类型会返回std::suspend_always
(对于延迟启动的协程)或std::suspend_never
(对于立即启动的协程)。 - 当
co_await promise.initial_suspend()
恢复执行时,开始执行协程的主体代码。
co_await
co_await
是 单目操作符,用于挂起协程的执行,并将控制权交回给调用者。它的操作数可以是以下两种类型之一的表达式:(1) 定义了成员 co_await
操作符的类类型,或者可以传递给非成员 co_await
操作符的类型;(2) 可以通过当前协程的 Promise::await_transform
方法转换为上述类类型的表达式。
co_await expr
(协程等待表达式) co_await
表达式只能出现在常规函数体中的潜在求值表达式里(包括 lambda 表达式的函数体),并且不能出现在以下位置:
- 异常处理程序中,
- 声明语句中,除非它作为该声明语句的初始化器出现,
- 初始化语句的简单声明中(例如
if
、switch
、for
和范围for
),除非它作为该初始化语句的初始化器出现, - 默认参数中,
- 具有静态或线程存储期的块作用域变量的初始化器中。
首先,expr
被转换成一个可等待对象(awaitable),转换过程如下:
- 如果
expr
是由初始挂起点(promise.initial_suspend()
)、最终挂起点(promise.final_suspend()
)或co_yield
表达式生成的,则可等待对象就是expr
本身。 - 否则,如果当前协程的
Promise
类型提供了await_transform
成员函数,则可等待对象是调用promise.await_transform(expr)
的结果。 - 否则,可等待对象就是
expr
本身。
然后,可以通过以下方法获取等待器对象(awaiter):
- 如果对
operator co_await
的重载解析找到一个最佳匹配,则等待器是该最佳匹配调用的结果:- 成员重载:
awaitable.operator co_await()
- 非成员重载:
operator co_await(static_cast<Awaitable&&>(awaitable))
- 成员重载:
- 如果重载解析没有找到
operator co_await
,则等待器就是awaitable
本身。 - 如果重载解析结果是模糊的,则程序是格式错误的。
如果上述表达式是一个纯右值(prvalue),则等待器对象是从它实例化的临时对象。否则,如果表达式是一个引用左值(glvalue),则等待器对象就是它所引用的对象。
然后,调用 awaiter.await_ready()
(这是一个避免挂起成本的快捷方式,如果已知结果已准备好或可以同步完成)。如果调用结果在上下文中转换为布尔值后为 false
,则:
- 协程被挂起(其协程状态被填充为局部变量和当前挂起点)。
- 调用
awaiter.await_suspend(handle)
,其中handle
是代表当前协程的协程句柄。在该函数内部,可以通过该句柄观察到挂起的协程状态,并且该函数负责安排它在某个执行器上恢复,或被销毁(返回false
视为已安排)。- 如果
await_suspend
返回void
,则立即将控制权返回给当前协程的调用者/恢复者(此协程保持挂起状态)。 - 如果
await_suspend
返回布尔值,- 返回
true
将控制权返回给当前协程的调用者/恢复者。 - 返回
false
恢复当前协程。
- 返回
- 如果
await_suspend
返回某个其他协程的协程句柄,则该句柄被恢复(通过调用handle.resume()
)(注意这可能最终导致当前协程恢复)。 - 如果
await_suspend
抛出异常,则捕获该异常,恢复协程,然后立即重新抛出该异常。
- 如果
- 最后,无论协程是否被挂起,都调用
awaiter.await_resume()
,其结果就是整个co_await expr
表达式的结果。
如果协程在 co_await
表达式中被挂起,并稍后恢复,则恢复点位于调用 awaiter.await_resume()
之前。
请注意,由于协程在进入 awaiter.await_suspend()
之前已完全挂起,因此该函数可以在没有额外同步的情况下自由地跨线程传递协程句柄。例如,它可以将句柄放入回调中,安排在线程池中的某个线程上执行异步 I/O 操作完成后的回调。在这种情况下,由于当前协程可能已经被恢复,并且执行了等待器对象的析构函数,因此在当前线程上 await_suspend()
继续执行的同时,所有并发操作 await_suspend()
应该将 *this
视为已销毁,并且在句柄发布到其他线程后不再访问它。
awaitable与awaiter
awaitable
和 awaiter
是 C++20 协程特性中的两个相关但不同的概念:
-
Awaitable:
awaitable
是一个可以被co_await
表达式等待的对象。- 它是一个类型,这个类型的对象能够表达等待的概念,并且知道如何与
co_await
表达式协作。 awaitable
类型必须提供operator co_await
,这个操作符返回一个awaiter
对象。awaitable
类型可以是任何类型,只要它或者通过成员函数或者通过转换操作能够提供与co_await
表达式协作的机制。
-
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
-
co_return
语句:当协程执行到co_return
语句时,它将完成其执行流程。co_return
可以单独使用,也可以与一个表达式一起使用来返回一个值。 -
返回值处理:
- 如果使用
co_return;
(没有表达式),则会调用协程的promise
类的return_void()
方法。 - 如果使用
co_return expr;
(表达式expr
类型为void
),同样会调用return_void()
。 - 如果使用
co_return expr;
(表达式expr
类型非void
),则会调用promise
类的return_value(expr)
方法,并将表达式的值作为参数传递。promise
会自动保存该值。
- 如果使用
-
最终挂起点:
co_return
之后,会调用promise
类的final_suspend()
方法。final_suspend()
也需要返回一个awaiter.