一、c++20的协程概念
在c++20标准后,在一些函数中看到co_await、co_yield、co_return这些关键词,这是c++20为协程实现设计的运算符。
协程是能暂停执行以在之后恢复的函数。原来我们调用一个功能函数时,只要调用了以后,就要完整执行完该功能函数所有步骤(语句)才能回来执行自身的步骤,对于一些功能函数其由很长的执行周期,该执行周期中调用者就不能处理自身一些事务。在协程出现以前我们就需要回调、阻塞等手段综合设计实现。
协程就是为了解决类似这种问题的。调用者调用协程函数后,执行到中途可以通过co_yield暂停挂起,返回自身执行事务,然后在通过resume唤醒恢复协程,协程函数会从挂起标识处继续往下执行。整个协程函数执行周期内,可以多次返回调用自身。在协程函数结束后,通过co_return还可以返回协程结果或协程内部对象。
协程是无栈的:它们通过返回到调用方暂停执行,并且从栈分离存储恢复执行需要的数据。这样就可以编写异步执行的顺序代码(例如,不使用显式的回调来处理非阻塞 I/O),还支持对惰性计算的无限序列上的算法及其他用途。
例如用关键词 co_yield
暂停执行并返回一个值:
task coroutine_func(int n = 1)
{
int i = 0;
while(i<n){
co_yield i++;
}
}
或者用关键词 co_return 完成执行并返回一个值或void:
task coroutine_func(int n = 1)
{
co_return;
}
二、c++20协程库
这些关键词都做了啥事情呢。在c++20协程库中,提供了以下支持库来实现协程:
协程特征,定义于头文件 <coroutine>
coroutine_traits (C++20)用于发现协程承诺类型的特征类型(类模板)
协程柄,定义于头文件 <coroutine>
coroutine_handle (C++20)用于指代暂停或执行的协程(类模板)
无操作协程,定义于头文件 <coroutine>
noop_coroutine (C++20)创建在等待或销毁时无可观察作用的协程柄(函数)
noop_coroutine_promise (C++20)用于无可观察作用的协程(类)
noop_coroutine_handle (C++20)std::coroutine_handle<std::noop_coroutine_promise> ,有意用于指代无操作协程
(typedef)
平凡可等待体,定义于头文件 <coroutine>
suspend_never (C++20)指示 await 表达式应该决不暂停(类)
suspend_always (C++20)指示 await 表达式应该始终暂停(类)
std::coroutine_traits从协程的返回类型与形参类型确定承诺类型。
//定义于头文件 <coroutine> ,(C++20 起)
template< class R, class... Args > struct coroutine_traits;
/*模板形参
*R - 协程的返回类型
*Args - 协程的形参类型,若协程为非静态成员函数则包括隐式对象形参
*/
标准库实现提供与 R::promise_type 相同的公开可访问成员类型 promise_type ,若该有限定标识合法并代表类型。否则它无成员。coroutine_traits 的程序定义特化应当定义公开可访问的成员类型 promise_type ,否则行为未定义。
//成员类型
//类型 定义
promise_type R::promise_type //若它合法,或由程序定义特化提供
2.1 coroutine_handle句柄
其中最主要的就是coroutine_handle句柄,类模板 coroutine_handle 能用于指代暂停或执行的协程,定义于头文件 <coroutine>:
//(C++20 起)
/*结构体主模板,可从 Promise 类型的承诺对象创建。*/
template< class Promise = void > struct coroutine_handle;
/*特化std::coroutine_handle<void>擦除承诺类型。它可从其他特化转换*
template<> struct coroutine_handle<void>;
/*特化std::coroutine_handle<std::noop_coroutine_promise>指代无操作协程。不能从承诺对象创建它*/
template<> struct coroutine_handle<std::noop_coroutine_promise>;
using noop_coroutine_handle =
std::coroutine_handle<std::noop_coroutine_promise>;
std::coroutine_handle 的每个特化均为可平凡复制 (TriviallyCopyable) ,并保有一个指向协程状态的指针作为其仅有的非静态成员。添加 coroutine_handle 的特化的程序行为未定义。std::coroutine_handle 功能如下:
//(C++20)
成员函数
(构造函数) 构造 coroutine_handle 对象(公开成员函数)
operator= 赋值 coroutine_handle 对象(公开成员函数)
from_promise [静态]从协程的承诺对象创建 coroutine_handle(公开静态成员函数)
转换
operator coroutine_handle<> 获得擦除类型的 coroutine_handle(公开成员函数)
观察器
done 检查协程是否已完成(公开成员函数)
operator bool 检查柄是否表示协程(公开成员函数)
控制
operator()
resume 恢复协程执行(公开成员函数)
destroy 销毁协程(公开成员函数)
承诺访问
promise 访问协程的承诺对象(公开成员函数)
导出/导入
address 导出底层地址,即支撑协程的指针(公开成员函数)
from_address [静态]从指针导入协程(公开静态成员函数)
非成员函数
operator== 比较二个 coroutine_handle 对象(函数)
operator<=> 比较二个 coroutine_handle 对象(函数)
辅助类
std::hash<std::coroutine_handle> std::coroutine_handle 的散列支持(类模板特化)
std::coroutine_handle,协程句柄是从协程外部操纵的,这是用于恢复协程执行或销毁协程帧的非拥有柄;承诺(promise)对象,从协程内部操纵,协程通过此对象提交其结果或异常。
2.2 std::coroutine_handle实现案例
现在来看如何通过std::coroutine_handle实现协程函数的,下面定义一个简单的协程例子:
//test0.h
#ifndef _TEST_0_H_
#define _TEST_0_H_
void coroutine_first_test(void);
#endif //_TEST_0_H_
//test0.cpp
#include "test0.h"
#include <coroutine>
#include <iostream>
struct task {
struct promise_type {
task get_return_object() {
std::cout << "task::promise_type.get_return_object \n";
return task{Handle::from_promise(*this)};
}
//返回std::suspend_never(这个随后说明) ,初始化后就继续运行
std::suspend_never initial_suspend() {
std::cout << "task::promise_type.initial_suspend \n";
return {};
}
std::suspend_never final_suspend() noexcept {
std::cout << "task::promise_type.final_suspend \n";
return {};
}
std::suspend_always yield_value(const int &val) noexcept { //co_yield调用
std::cout << "task::promise_type.yield_value " << val << "\n";
return {};
}
void return_void() {} //co_return调用
void unhandled_exception() {
std::cout << "task::promise_type.unhandled_exception \n";
}
};
using Handle = std::coroutine_handle<promise_type>;//协程句柄
explicit task(Handle coroutine) : m_coroutine{coroutine} {} //get_return_object时调用
task() = default;
~task() {
std::cout << "~task \n";
if (m_coroutine) //自行销毁
{
m_coroutine.destroy();
}
}
// task(const task&) = delete;
// task& operator=(const task&) = delete;
Handle m_coroutine; //协程句柄
};
task coroutine_func(int n = 0)
{
int i = 0;
while(i<n){
co_yield i++;
std::cout << "coroutine dosomthing" << i << "\n";
}
co_return;
}
void coroutine_first_test(void)
{
auto c0_obj = coroutine_func(10);
for (size_t i = 0; i < 5; i++)
{
c0_obj.m_coroutine.resume();//唤醒协程
std::cout << "caller dosomthing" << i << "\n";
}
};
//main.cpp
#include "test0.h"
int main(int argc, char* argv[])
{
coroutine_first_test();
return 0;
};
task是个自定义的结构体,为了能作为协程的返回值,需要定义一个内部 promise_type结构体。
【1】协程开始执行时,它进行下列操作:
- 用 operator new 分配协程状态对象(
coroutine state
)。 - 将所有函数形参复制到协程状态中:按值传递的形参被移动或复制,按引用传递的参数保持为引用(因此,如果在被指代对象的生存期结束后恢复协程,它可能变成悬垂引用),上述例如传入的是int型引用。
- 调用承诺对象的构造函数(
promise
)。如果承诺类型拥有接收所有协程形参的构造函数,那么以复制后的协程实参调用该构造函数。否则调用其默认构造函数。这里采用默认构造函数,没具名给出。 - 调用 promise.get_return_object() 并将其结果在局部变量中保持。该调用的结果将在协程首次暂停时返回给调用方。至此并包含这个步骤为止,任何抛出的异常均传播回调用方,而非置于承诺中。
- 调用 promise.initial_suspend() 。典型的承诺类型要么(对于惰性启动的协程)返回 std::suspend_always,要么(对于急切启动的协程)返回 std::suspend_never。
- *如果返回std::suspend_never,表示await表达式应该决不暂停,立即执行,不挂起
- *返回std::suspend_always,表示await表达式应该始终暂停,不立即执行,先挂起
- 当 co_await promise.initial_suspend() 恢复时,开始协程体的执行。
【2】当协程抵达暂停点时:
- 将先前获得的返回对象返回给调用方/恢复方,这里是coroutine_first_test函数,如果需要则先隐式转换到协程的返回类型。
- 协程从暂停点返回通过co_yield,本质上是调用了promise.yield_value(表达式)
- 调用方/恢复方,coroutine_first_test函数通过调用coroutine_handle的resume告知协程恢复执行,协程函数coroutine_func重新从co_yield语句的下一句开始执行。
【3】当协程抵达 co_return 语句时,它进行下列操作:
- 若是co_return,调用 promise.return_void(),如果承诺类型 Promise 没有 Promise::return_void() 成员函数(本例所采用),那么则行为未定义。
- 若是co_return expr,调用 promise.return_value(expr),其中 expr 具有非 void 类型或 void 类型,如果承诺类型 Promise 没有 Promise::return_value() 成员函数(本例子没定义),那么则行为未定义。
- 控制流出返回时,协程结束开始结束运行。此时以创建的逆序销毁所有具有自动存储期的变量。
- 调用 promise.final_suspend() 。
【4】如果协程因未捕捉的异常结束,那么它进行下列操作:
- 捕捉异常并在 catch 块内调用 promise.unhandled_exception()
- 调用 promise.final_suspend() (例如,以恢复某个继续或发布其结果)。此时开始恢复协程是未定义行为。
【5】协程当经由 co_return 或未捕捉异常而正常终止,它进行下列操作:
- 调用承诺对象的析构函数(~promise_type,默认析构)。
- 调用各个函数形参副本的析构函数(本例只有int型引用)。
- 调用 operator delete 以释放协程状态所用的内存(~task)。
- 转移执行回到调用方/恢复方(coroutine_first_test)。
这里的协程函数返回对象采用的是 std::suspend_never等待体,标准库定义了两个平凡的可等待体:std::suspend_always 及 std::suspend_never,先说std::suspend_never:
//std::suspend_never,定义于头文件 <coroutine>,
//suspend_never 是空类,能用于指示 await 表达式绝不暂停并且不产生值。
/*成员函数*/
/*(C++20 起)
*std::suspend_never::await_ready,指示 await 表达式绝不暂停(公开成员函数)
*始终返回 true ,指示 await 表达式绝不暂停。
*/
constexpr bool await_ready() const noexcept { return true; }
/*(C++20 起)
*std::suspend_never::await_suspend,无操作(公开成员函数)
*不做任何事。
*/
constexpr void await_suspend() const noexcept {}
/*(C++20 起)
*std::suspend_never::await_resume,无操作(公开成员函数)
*不做任何事。若使用 suspend_never 则 await 表达式不产生值。
*/
constexpr void await_resume() const noexcept {}
编译g++ main.cpp test*.cpp -o test.exe -std=c++20,运行程序很好展示了上述逻辑过程:
2.3 承诺类型(Promise)
承诺类型(Promise),获得到承诺对象的引用。若 *this 不指代承诺对象尚未被销毁的协程,则行为未定义。此函数不对特化 std::coroutine_handle<> 提供。
//std::coroutine_handle<Promise>::promise
//主模板的成员
Promise& promise() const;
//特化 std::coroutine_handle<std::noop_coroutine_promise> 的成员
std::noop_coroutine_promise& promise() const noexcept;
编译器用 std::coroutine_traits 从协程的返回类型确定承诺类型。
正式而言,,如果定义它为非静态成员函数,以如下方式确定它的承诺类型 :
/*
*令 R 与 Args... 分别代表协程的返回类型与参数类型列表,
*ClassT 与 /*cv限定*/ (如果存在)分别代表协程所属的类与其 cv 限定
*/
std::coroutine_traits<R, Args...>::promise_type //如果不定义协程为非静态成员函数。
std::coroutine_traits<R, ClassT /*cv限定*/&, Args...>::promise_type //如果定义协程为非右值引用限定的非静态成员函数。
◦std::coroutine_traits<R, ClassT /*cv限定*/&&, Args...>::promise_type //如果定义协程为右值引用限定的非静态成员函数。
例如,如果上述结构体task定义为结构体模板,template<typename T> struct task,其协程函数定义:
//如果定义协程为
task<float> foo(std::string x, bool flag);
//那么它的承诺类型是
std::coroutine_traits<task<float>, std::string, bool>::promise_type。
//如果定义协程为
task<void> my_class::method1(int x) const;
//那么它的承诺类型是 std::coroutine_traits<task<void>, const my_class&, int>::promise_type。
//如果定义协程为
task<void> my_class::method1(int x) &&;
//那么它的承诺类型是
std::coroutine_traits<task<void>, my_class&&, int>::promise_type。
每个协程均与下列对象关联:
- 承诺(promise)对象。
- 协程句柄 (coroutine handle)。
- 协程状态 (coroutine state),它是一个包含以下各项的分配于堆(除非优化掉其分配)的内部对象:
- 承诺对象
- 各个形参(全部按值复制)
- 当前暂停点的某种表示,使得恢复时程序知晓要从何处继续,销毁时知晓有哪些局部变量在作用域内
- 生存期跨过当前暂停点的局部变量和临时量
协程状态由非数组 operator new 在堆上分配。如果承诺类型定义了类级别的替代函数,那么会使用它,否则会使用全局的 operator new;如果承诺类型定义了接收额外形参的 operator new 的布置形式,且它们所匹配的实参列表中的第一实参是要求的大小(std::size_t 类型),而其余则是各个协程函数实参,那么将这些实参传递给 operator new(这使得能对协程使用前导分配器约定)
如果分配失败,那么协程抛出 std::bad_alloc,除非承诺类型 Promise 类型定义了成员函数 Promise::get_return_object_on_allocation_failure()。如果定义了该成员函数,那么使用 operator new 的 nothrow 形式进行分配,而在分配失败时,协程会立即将从 Promise::get_return_object_on_allocation_failure() 获得的对象返回给调用方。
2.4 承诺类型-协程返回类型及协程函数的交互
下来看一下一个更复杂的例子,可以从协程传递( co_yield、co_return)回引用,实现协程与调用者的交互。
//test1.h
#ifndef _TEST_1_H_
#define _TEST_1_H_
void coroutine_model_test(void);
#endif //_TEST_1_H_
//test1.cpp
#include "test1.h"
#include <coroutine>
#include <iostream>
#include <optional>
#include <ranges>
template<typename T> requires std::movable<T>
class Task {
public:
//promise_type就是承诺对象,承诺对象用于协程内外交流
struct promise_type {
//生成协程返回,会在协程正在运行前进行调用
Task<T> get_return_object() {
std::cout << "get_return_object \n";
return Task{Handle::from_promise(*this)};
}
/*
*返回的就是std::suspend_always,在协程被创建及真正运行前,被调用
*/
static std::suspend_always initial_suspend() noexcept {
std::cout << "initial_suspend \n";
return {};
}
//返回awaiter,在协程最后退出后调用的接口。
static std::suspend_always final_suspend() noexcept {
std::cout << "final_suspend \n";
return {};
}
//返回awaiter,会在 co_yield v 时被调用类型就是T,v就是传入参数value
std::suspend_always yield_value(T value) noexcept //-4-
{
current_value = std::move(value);
std::cout << "yield_value ";
return {};
}
//会在 co_return v 时被调用,把 co_return 后面跟着的值value作为参数传入,这里一般就是把这个值保存下来,提供给协程调用者
void return_value(const T& value)
{
std::cout << "return_value call "<< value << "\n";//cout危险操作,取决于T类型,这里为了展示原理
current_value = std::move(value);
return;
}
//会在 co_return v 时被调用,无传入值,和return_value只选一个,否则会报编译错误
/*
void return_void()
{
std::cout << "return void invoked." << std::endl;
}
*/
// 生成器协程中不允许 co_await 。
void await_transform() = delete;
//协程内的代码抛出了异常,这个接口会被调用
static void unhandled_exception() {
std::cout << "unhandled_exception ";
throw;
}
std::optional<T> current_value;
};
using Handle = std::coroutine_handle<promise_type>;//协程句柄
explicit Task(Handle coroutine) :
m_coroutine{coroutine}
{}
Task() = default;
~Task() {
std::cout << "~Task \n";
if (m_coroutine) //自行销毁
{
m_coroutine.destroy();
}
}
Task(const Task&) = delete;
Task& operator=(const Task&) = delete;
Task(Task&& other) noexcept :
m_coroutine{other.m_coroutine}
{
other.m_coroutine = {};
}
Task& operator=(Task&& other) noexcept {
if (this != &other) {
m_coroutine = other.m_coroutine;
other.m_coroutine = {};
}
return *this;
}
// 基于范围的 for 循环支持。通过迭代操作实现协程应用
class Iter {
public:
void operator++() //-6-
{
std::cout << "real resume ";
m_coroutine.resume(); //++时,恢复协程
}
const T& operator*() const {
return *m_coroutine.promise().current_value; //取值,通过promise获取数据,返回值T
}
bool operator==(std::default_sentinel_t) const {
return !m_coroutine || m_coroutine.done(); //赋值时,协程执行
}
explicit Iter(Handle coroutine) :
m_coroutine{coroutine}
{}
private:
Handle m_coroutine;//协程句柄
};
//range应用指定的开闭区间
Iter begin() {
if (m_coroutine) {
m_coroutine.resume();
}
return Iter{m_coroutine};
}
std::default_sentinel_t end() {
return {};
}
const T& get_val()
{
return *m_coroutine.promise().current_value;
}
private:
Handle m_coroutine; //协程句柄
};
//协程函数的返回值为Task<T>类型,协程的返回类型必须内部定义Task<T>::promise_type
template<std::integral T>
Task<T> range(T first, T last)
{
T sum = T();
while (first < last) //-2-
{
sum += first;
co_yield first++;//协程会挂起,返回值;等价于co_await promise.yield_value(表达式)。-3-
//调用者resume时,在此处恢复执行
std::cout << "co_yield\n";//-7-
}
co_return sum;//等价于co_await promise.return_value(表达式)
};
void coroutine_model_test(void)
{
auto rs = range(-4, 4);
for (int i : rs) //-1-,
// for (int i : range(-4, 4)) //-1-,
{
std::cout << i << " ";//-5-
}
std::cout << "\n";
std::cout << "rs last val = " << rs.get_val() << "\n";
};
//main.cpp
#include "test1.h"
int main(int argc, char* argv[])
{
coroutine_model_test();
return 0;
};
本案例定义了一个for循环的范围返回函数,该范围是Task类的begin()和end()函数提供,而函数引用了内置类型Iter,Iter在迭代递增时(operator++()),会调用std::coroutine_handle的resume进行协程恢复。协程运行到“co_yield first++;”时,就会通过yield_value设置了promise内部的缓存值,并返回,而调用者函数coroutine_model_test则通过std::coroutine_handle句柄获知promise承诺对象及内部值(及遍历数值),在遍历时,每次递增,本质上会调用std::coroutine_handle的resume告知协程恢复执行,而协程每次进行递增数值会写入promise承诺对象内部,如此反复等同于coroutine_model_test获得遍历范围值。
协程最终返回时“co_return sum;”,传递回来一个数值,本质上是通过promise承诺对象内部return_value函数实现的。因为通过“current_value = std::move(value);”将最后传递进入的值保存在promise承诺对象内部,因此在调用函数内通过get_val就能取得该缓存的值。
const T& Task<T>::get_val()
{
return *m_coroutine.promise().current_value;
}
上述例子中通过“-*-”标识了协程调用逻辑次序,编译g++ main.cpp test*.cpp -o test.exe -std=c++20,运行测试:
Task内部promise 类的工作主要是两个:
- 从协程的承诺对象创建 coroutine_handle,接口是get_return_object。
- 是定义协程的执行流程,主要接口是initial_suspend,final_suspend。
- 是负责协程和调用者之间的数据传递,主要接口是 yield_value 和return_value或return_void。
通常,promise_type类型需要主要实现这几个接口:
【1】Task<T> get_return_object () 这个接口要能用 promise 自己的实例构造出一个协程的返回值,会在协程正在运行前进行调用,这个接口的返回值会作为协程的返回值。
【2】std::suspend_always initial_suspend () 这个接口会在协程被创建(也就是第一次调用),真正运行前,被调用。在上述这个例子里,指定返回空类,指示 await 表达式始终暂停并且不产生值。
return {};
std::suspend_always是一个结构体,前面已经说明了std::suspend_never,下来看看std::suspend_always,它和std::suspend_never几乎一样:
/* (C++20 起)
*std::suspend_always,定义于头文件 <coroutine>
*suspend_always 是空类,能用于指示 await 表达式始终暂停并且不产生值。
*/
struct suspend_always;
该类包含几个成员函数,用来判定
成员函数
/*(C++20) 指示 await 表达式始终暂停(公开成员函数)
*std::suspend_always::await_ready
*始终返回 false ,指示 await 表达式始终暂停。
*/
constexpr bool await_ready() const noexcept { return false; }
/*(C++20) 无操作(公开成员函数)
*std::suspend_always::await_suspend
*不做任何事。
*/
constexpr void await_suspend() const noexcept {}
(C++20 起)
/*(C++20) 无操作(公开成员函数)
*std::suspend_always::await_resume
*不做任何事。若使用 suspend_always 则 await 表达式不产生值。
*/
constexpr void await_resume() const noexcept {}
【3】std::suspend_always yield_value (T v) 这个接口会在 co_yield v 时被调用,把 co_yield 后面跟着的值 v 做为参数传入,这里一般就是把这个值保存下来,提供给协程的调用者,返回值一般是std::suspend_always {}。
【4】void return_value (T v) 这个接口会在 co_return v 时被调用,把 co_return 后面跟着的值 v 作为参数传入,这里一般就是把这个值保存下来,提供给协程调用者。
【5】void return_void () 如果 co_return 后面没有接任何值,那么就会调用这个接口。return_void 和return_value 只能选择一个实现,否则会报编译错误。
【6】std::suspend_always final_suspend () 在协程最后退出后调用的接口,如果返回 std::suspend_always 。
【7】协程结束后,则需要用户自行调用 coroutine_handle 的 destroy 接口来释放协程相关的资源。若协程对应的 handle 就已经为空,不能再调用 destroy 了 (会 coredump)。
【8】void unhandled_exception () 如果协程内的代码抛出了异常,那么这个接口会被调用。
std::coroutine_handle<promise_type> 是协程的控制句柄类,也是协程函数返回类型的最重要成员,通过标准库里std::coroutine_handle结构体定义,就可以实现与承诺对象的交互能力。恢复协程、销毁协程实例等都是通过该句柄实现。
2.5 co_await
一元运算符 co_await 暂停协程并将控制返回给调用方。它的操作数是一个函数表达式:
co_await 函数表达式
函数表达式,即函数,其返回一个类似于std::suspend_always可等待结构体(awaitable),就是需要像std::suspend_always一样为该结构体定义await_ready、await_suspend、await_resume函数:
- 协程函数resuming_on_new_thread,调用函数表达式fun(switch_to_new_thread),fun返回结果,就是一个等待体(这里是awaitable)。
- 开始调用 await_ready()。如果它的结果按语境转换成 bool 为 false,那么:暂停协程(以各局部变量和当前暂停点填充其协程状态),然后调用 await_suspend 接口,并将协程的句柄传给这个接口。
- 如果await_ready 返回 true,那么协程完全不会被挂起,直接会去调用 await_resume () 接口,把这个接口作为 await 的返回值,继续执行协程。
- 调用 await_suspend(handle),其中 handle 是表示当前协程的协程句柄。这个函数内部可以通过这个句柄观察暂停的协程,而且此函数负责调度它以在某个执行器上恢复,或将其销毁(并返回 false 当做调度) ◦
- 如果 await_suspend 返回 void,那么立即将控制返回给当前协程的调用方/恢复方(此协程保持暂停),否则如果 await_suspend 返回 bool,那么:
- 值为 true 时将控制返回给当前协程的调用方/恢复方
- 值为 false 时恢复当前协程。
- 如果 await_suspend 返回某个其他协程的协程句柄,那么(通过调用 handle.resume())恢复该句柄(注意这可以连锁进行,并最终导致当前协程恢复)
- 如果 await_suspend 抛出异常,那么捕捉该异常,恢复协程,并立即重抛异常
- 最后,调用 await_resume(),它的结果就是整个 co_await expr 表达式的结果。
- 如果协程在 co_await 表达式中暂停而在后来恢复,那么恢复点处于紧接对 await_resume() 的调用之前。
注意,因为协程在进入 await_suspend() 前已经完全暂停,所以该函数可以自由地在线程间转移协程柄而无需额外同步。例如,可以将它放入回调,将它调度成在异步 I/O 操作完成时在线程池上运行等。此时因为当前协程可能已被恢复,从而执行了等待器的析构函数,同时由于 await_suspend() 在当前线程上持续执行,await_suspend() 应该把 *this 当作已被销毁并且在柄被发布到其他线程后不再访问它。
2.6 co_await案例
下面例子,定义了一个awaitable,具有定义await_ready、await_suspend、await_resume函数成员函数,通过switch_to_new_thread函数表达式返回co_await。
//test2.h
#ifndef _TEST_2_H_
#define _TEST_2_H_
void coroutine_wait_test(void);
#endif //_TEST_2_H_
//test2.cpp
#include "test2.h"
#include <coroutine>
#include <iostream>
#include <stdexcept>
#include <thread>
auto switch_to_new_thread(std::jthread& out)
{
struct awaitable
{
std::jthread* p_out;
//co_await开始会调用,根据返回值决定是否挂起协程
bool await_ready() { return false; }
//在协程挂起后会调用这个,如果返回true,会返回调用者,如果返回false,会立刻resume协程
void await_suspend(std::coroutine_handle<> h)
{
std::jthread& out = *p_out;
if (out.joinable())
throw std::runtime_error("jthread out arg is unull");
out = std::jthread([h] { h.resume(); });//新建线程,并协程恢复
std::cout << "new thread ID:" << out.get_id() << "\n"; //
}
//在协程resume的时候会调用这个,这个的返回值会作为await的返回值
void await_resume() {}
};
return awaitable{&out};
}
struct Task{
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};
Task resuming_on_new_thread(std::jthread& out) {
std::cout << "initial,ID:" << std::this_thread::get_id() << "\n";
co_await switch_to_new_thread(out);//协程等待
// 等待器在此销毁
std::cout << "final,ID:" << std::this_thread::get_id() << "\n";
}
void coroutine_wait_test(void)
{
std::jthread out;
auto ret = resuming_on_new_thread(out);
};
//main.cpp
#include "test2.h"
int main(int argc, char* argv[])
{
coroutine_wait_test();
return 0;
};
编译g++ main.cpp test*.cpp -o test.exe -std=c++20及运行程序:
2.7 无操作协程
前面描述协程支持库就提到过无操作协程,相比一般协程,它体现如此特征:在协程控制流外不做任何事,在开始和恢复后立即暂停,拥有一种协程状态,而销毁该状态为无操作,若有任何指代它的 std::coroutine_handle 则绝不抵达其最终暂停点。
无操作协程,定义于头文件 <coroutine>
noop_coroutine (C++20)创建在等待或销毁时无可观察作用的协程柄(函数)
noop_coroutine_promise (C++20)用于无可观察作用的协程(类)
noop_coroutine_handle (C++20)std::coroutine_handle<std::noop_coroutine_promise> ,有意用于指代无操作协程
(typedef)
std::noop_coroutine_promise是是无操作协程的承诺类型,本质上就是一个前面讲述的空promise_type结构体:
//定义于头文件 <coroutine>
struct noop_coroutine_promise {};
而std::noop_coroutine_handle就是std::coroutine_handle句柄以std::noop_coroutine_promise为承诺对象的特例化,
//定义于头文件 <coroutine>,(C++20 起)
template< class Promise = void > struct coroutine_handle;
template<> struct coroutine_handle<std::noop_coroutine_promise>;
using noop_coroutine_handle = std::coroutine_handle<std::noop_coroutine_promise>;
std::noop_coroutine是一个函数,用来返回指代无操作协程的协程柄。
/*std::noop_coroutine,定义于头文件 <coroutine>,(C++20 起)
*返回值指代无操作协程的 std::noop_coroutine_handle
*若已有无操作协程的协程状态,则不指定 noop_coroutine 的后续调用是返回先前获得的协程柄,
*还是指代新的无操作协程的协程状态的协程柄。
*/
std::noop_coroutine_handle noop_coroutine() noexcept;
2.8 无操作协程案例
这是协程函数嵌套的例子,该例子里协程函数test调用了协程函数get_random,它们的返回值都是Task<int>。协程返回类型内,定义了一个可等待体awaiter,在co_await调用时开始触发。另外还为协程返回类型Task定义了承诺类型promise_type,及在该承诺类型内定义了一个可等待体final_awaiter,它会在承诺类型调用final_suspend时构建和开发触发。可等待体final_awaiter的await_suspend函数在传递协程句柄有效时直接返回,以恢复先前的协程;否则返回 noop_coroutine() ,其恢复不做任何事。
//test3.h
#ifndef _TEST_3_H_
#define _TEST_3_H_
void coroutine_noop_test(void);
#endif //_TEST_3_H_
//test3.cpp
#include "test3.h"
#include <coroutine>
#include <utility>
#include <iostream>
template<class T>
struct Task {
struct promise_type {//承诺类型
promise_type() : result(T()),previous(std::noop_coroutine())
{
std::cout << "in Task::promise_type()\n";
};
auto get_return_object() {
std::cout << "in Task::promise_type::get_return_object()\n";
return Task(std::coroutine_handle<promise_type>::from_promise(*this));
}
//返回std::suspend_always{} ,表示await表达式应该始终暂停,不立即执行,先挂起
std::suspend_always initial_suspend() {
std::cout << "in Task::promise_type::initial_suspend()\n";
return {};
}
//可等待体定义
struct final_awaiter {
//co_await开始会调用,根据返回值决定是否挂起协程
bool await_ready() noexcept(true) {
std::cout << "in Task::promise_type::final_awaiter::await_ready()\n";
return false;
}
//在协程挂起后会调用这个,如果返回true,会返回调用者,如果返回false,会立刻resume协程
std::coroutine_handle<> await_suspend(std::coroutine_handle<promise_type> h) noexcept(true)
{
// 在当前协程(以 'h' 指代)执行即将结束时调用 final_awaiter::await_suspend 。
// 若当前协程被另一协程经由 co_await get_Task() 恢复,则存储到该协程的柄
// 为 h.promise().previous 。该情况下,返回柄以恢复先前的协程。
// 否则返回 noop_coroutine() ,其恢复不做任何事。
std::cout << "in Task::promise_type::final_awaiter::await_suspend()\n";
std::cout << "T = " << h.promise().result << "\n";//co_return *传递的值,不规范语句,主要为了测试逻辑展示
auto previous = h.promise().previous;
if (previous) {
return previous;
} else {
return std::noop_coroutine();
}
}
//在协程resume的时候会调用这个,这个的返回值会作为final_awaiter的返回值
void await_resume() noexcept(true) {
std::cout << "in Task::promise_type::final_awaiter::await_resume()\n";
}
};
//返回final_awaiter{},Task结束协程时,将进入promise_type.final_suspend,进入final_awaiter等待体执行逻辑
final_awaiter final_suspend() noexcept {
std::cout << "in Task::promise_type::final_suspend()\n";
return {};
}
void unhandled_exception() { throw; }
//会在 co_return v 时被调用,把这个v值保存下来,提供给协程调用者
void return_value(T value) {
std::cout << "in Task::promise_type::return_value()\n";
result += std::move(value);
}
T result;
std::coroutine_handle<> previous;
};
//
Task(std::coroutine_handle<promise_type> h) : coro(h) {//get_return_object函数内调用
std::cout << "in Task()\n";
}
Task(Task&& t) = delete;
~Task() {
std::cout << "in ~Task()\n";
coro.destroy();
}
//可等待体定义
struct awaiter {
//co_await开始会调用,根据返回值决定是否挂起协程
bool await_ready() {
std::cout << "in Task::awaiter::await_ready()\n";
return false; //挂起
}
//在协程挂起后会调用这个,如果返回true,会返回调用者,如果返回false,会立刻resume协程
auto await_suspend(std::coroutine_handle<> h) {
std::cout << "in Task::awaiter::await_suspend()\n";
coro.promise().previous = h;//将Task协程句柄指向的promise,其内部定义std::coroutine_handle<> 特例化句柄
return coro;
}
//在协程resume的时候会调用这个,这个的返回值会作为awaiter的返回值
T await_resume() {
std::cout << "in Task::awaiter::await_resume()\n";
return std::move(coro.promise().result);
}
std::coroutine_handle<promise_type> coro;
};
awaiter operator co_await() { //co_await调用
std::cout << "in Task::co_await()\n";
return awaiter{coro}; //将Task协程句柄传入
}
T operator()() {
std::cout << "in Task::operator()\n";
coro.resume();
return std::move(coro.promise().result);
}
private:
std::coroutine_handle<promise_type> coro;//协程句柄
};
//协程函数,返回Task<int>
Task<int> get_random() {
std::cout << "in get_random()\n";
co_return 4;
};
//协程函数,返回Task<int>
Task<int> test() {
Task<int> v = get_random(); //
Task<int> u = get_random();
std::cout << "in test()\n";
int x = (co_await v + co_await u);//相当于调用Task::co_await()
co_return x;
};
void coroutine_noop_test(void)
{
Task<int> t = test();//
int result = t();
std::cout << result << '\n';
};
//main.cpp
#include "test3.h"
int main(int argc, char* argv[])
{
coroutine_noop_test();
return 0;
};
编译g++ main.cpp test3.cpp -o test.exe -std=c++20,运行测试: