C/C++开发,无可避免的多线程(篇三).协程及其支持库
一、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就是传入参数valuestd::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,运行测试:

相关文章:
C/C++开发,无可避免的多线程(篇三).协程及其支持库
一、c20的协程概念 在c20标准后,在一些函数中看到co_await、co_yield、co_return这些关键词,这是c20为协程实现设计的运算符。 协程是能暂停执行以在之后恢复的函数。原来我们调用一个功能函数时,只要调用了以后,就要完整执行完该…...
高级信息系统项目管理(高项 软考)原创论文项目背景合集
以下为原创的高项论文项目背景合集5篇,建议自己以此为基础,再多多打磨完善一下,避免雷同,同时使项目背景更加真实可信。 一、某市智慧工地系统建设项目 某市住建局智慧工地系统建设项目是在该市住建局促进建筑行业转型升级和科技创新,强化工程质量安全,推动建筑业高质量…...
锁屏面试题百日百刷-Hive篇(十一)
锁屏面试题百日百刷,每个工作日坚持更新面试题。锁屏面试题app、小程序现已上线,官网地址:https://www.demosoftware.cn。已收录了每日更新的面试题的所有内容,还包含特色的解锁屏幕复习面试题、每日编程题目邮件推送等功能。让你…...
一看就懂,等保2.0工作流程这么做
等保2.0相关国家标准于2019年12月1日开始实施,标志着我国网络安全等级保护工作进入一个崭新的阶段,对于加强我国网络安全保障工作,提升网络安全保护能力具有十分重要的意义。很多行业主管单位要求行业客户开展等级保护工作,合理地…...
Kerberos 域委派攻击之非约束性委派
CSDN文章自动迁移自博客在Windows 2000 Server 首次发布 Active Directory 时,Microsoft 必须提供一种简单的机制来支持用户通过 Kerberos 向 Web Server 进行身份验证并需要代表该用户更新后端数据库服务器上的记录的方案。这通常称为“Kerberos 双跳问题”&#x…...
【容器运行时】一文理解 OCI、runc、containerd、docker、shim进程、cri、kubelet 之间的关系
参考 docker,containerd,runc,docker-shim 之间的关系Containerd shim 进程 PPID 之谜内核大神教你从 Linux 进程的角度看 DockerRunC 简介OCI和runCContainerd 简介从 docker 到 runCDockershim究竟是什么技术干货|Docker和 Con…...
spark兼容性验证
前言 Apache Spark是专门为大规模数据处理而设计的快速通用的计算引擎,Spark拥有Hadoop MapReduce所具有的优点,但不同于Mapreduce的是Job中间输出结果可以保存在内存中,从而不再需要读写HDFS,因此Spark能更好的适用于数据挖掘与…...
docker逃逸复现--pid=host模式下的逃逸
漏洞原理当docker以--pidhost模式启动时,你可以通过在容器进程中注入一些shellcode进行逃逸。相当于给了docker Linux中的CAP_SYS_PTRACE权限--pidhost:意味着宿主机与容器公享一套pid,如此做容器就可以访问并跟踪宿主机的进程Linux中的CAP_S…...
【环境配置】Windows系统下搭建Pytorch框架
【环境配置】Windows系统下搭建Pytorch框架 在Windows Serve 2019系统下搭建Pytorch框架 目录 【环境配置】Windows系统下搭建Pytorch框架1.用驱动总裁安装显卡驱动2.在cmd运行nvidia-smi3.安装cuda4.安装cudnn5.安装pytorch的命令1.首次安装2.操作失误需要重新安装6.安装torc…...
Dockerfile简单使用入门
什么是 Dockerfile? Dockerfile 是一个用来构建镜像的文本文件,文本内容包含了一条条构建镜像所需的指令和说明。 docker build命令用于从Dockerfile构建映像。可以在docker build命令中使用-f标志指向文件系统中任何位置的Dockerfile。 例如࿱…...
什么是CCC认证3C强制认证机构
什么是CCC认证3C强制认证机构? 3C认证的全称为“强迫性产物认证轨制”,它是中国政府为掩护消费者人身平安和国度平安、增强产物品质治理、按照法律法规履行的一种产物及格评定轨制。所谓3C认证,便是中国强迫性产物认证轨制,英文名…...
C语言-基础了解-18-C共用体
C共用体 一、共用体 共用体是一种特殊的数据类型,允许您在相同的内存位置存储不同的数据类型。您可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。共用体提供了一种使用相同的内存位置的有效方式 二、定义共同体 为了定义共用体&…...
Vue基础18之github案例、vue-resource
Vue基础18github案例静态页面第三方样式引入(以bootstrap举例)App.vueSearch.vueList.vue列表展示接口地址使用全局事件总线进行兄弟间组件通信Search.vueList.vue完善案例List.vueSearch.vue补充知识点:{...this.info,...this.dataObj}效果呈…...
UE4 c++ Mediaplayer取消自动播放,运行时首帧为黑屏的问题
0,前言 工作需要使用C制作一个ue4的视频插件,其中一个功能是能够选择 运行时是否自动播放 视频的功能。 在实现时遇见了一个问题,取消自动播放之后,运行时首帧是没有取到的,在场景里面看是黑色的。就这个问题我想到了使…...
C语言-基础了解-17-C结构体
C结构体一、c结构体C 数组允许定义可存储相同类型数据项的变量,结构是 C 编程中另一种用户自定义的可用的数据类型,它允许您存储不同类型的数据项。结构体中的数据成员可以是基本数据类型(如 int、float、char 等),也可…...
Python爬虫实践:优志愿 院校列表
https://www.youzy.cn/tzy/search/colleges/collegeList获取目标网址等信息打开开发人员工具(F12),拿到调用接口的地址,以及接口请求参数等信息,如下curl https://uwf7de983aad7a717eb.youzy.cn/youzy.dms.basiclib.ap…...
Java框架学习 | MySQL和Maven笔记
1.MySQL提问式思考 为什么要有数据库?MySQL的优劣势?Java的优劣势? JavaMySQL开源具有大量的社区成员和丰富的资源免费/具有大量的社区成员和丰富的资源可扩展性多态、继承和接口等分区、复制和集群等方式扩展数据库的容量和性能安全性有许…...
C++入门教程||C++ 变量作用域||C++ 常量
C 变量作用域 作用域是程序的一个区域,一般来说有三个地方可以声明变量: 在函数或一个代码块内部声明的变量,称为局部变量。在函数参数的定义中声明的变量,称为形式参数。在所有函数外部声明的变量,称为全局变量。 我…...
想找工作,这一篇15w字数+的文章帮你解决
文章目录前言一 专业技能1. 熟悉GoLang语言1.1 Slice1.2 Map1.3 Channel1.4 Goroutine1.5 GMP调度1.6 垃圾回收机制1.7 其他知识点2. 掌握Web框架Gin和微服务框架Micro2.1 Gin框架2.2 Micro框架2.3 Viper2.4 Swagger2.5 Zap2.6 JWT3. 熟悉使用 MySQL 数据库3.1 索引3.2 事务3.3…...
Mac brew搭建php整套开发环境
Homebrew完整版,安装时间较长/bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)"精简版/bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)" speednginxBrew sear…...
如何为服务器生成TLS证书
TLS(Transport Layer Security)证书是确保网络通信安全的重要手段,它通过加密技术保护传输的数据不被窃听和篡改。在服务器上配置TLS证书,可以使用户通过HTTPS协议安全地访问您的网站。本文将详细介绍如何在服务器上生成一个TLS证…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个生活电费的缴纳和查询小程序
一、项目初始化与配置 1. 创建项目 ohpm init harmony/utility-payment-app 2. 配置权限 // module.json5 {"requestPermissions": [{"name": "ohos.permission.INTERNET"},{"name": "ohos.permission.GET_NETWORK_INFO"…...
【C++从零实现Json-Rpc框架】第六弹 —— 服务端模块划分
一、项目背景回顾 前五弹完成了Json-Rpc协议解析、请求处理、客户端调用等基础模块搭建。 本弹重点聚焦于服务端的模块划分与架构设计,提升代码结构的可维护性与扩展性。 二、服务端模块设计目标 高内聚低耦合:各模块职责清晰,便于独立开发…...
在WSL2的Ubuntu镜像中安装Docker
Docker官网链接: https://docs.docker.com/engine/install/ubuntu/ 1、运行以下命令卸载所有冲突的软件包: for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done2、设置Docker…...
重启Eureka集群中的节点,对已经注册的服务有什么影响
先看答案,如果正确地操作,重启Eureka集群中的节点,对已经注册的服务影响非常小,甚至可以做到无感知。 但如果操作不当,可能会引发短暂的服务发现问题。 下面我们从Eureka的核心工作原理来详细分析这个问题。 Eureka的…...
VM虚拟机网络配置(ubuntu24桥接模式):配置静态IP
编辑-虚拟网络编辑器-更改设置 选择桥接模式,然后找到相应的网卡(可以查看自己本机的网络连接) windows连接的网络点击查看属性 编辑虚拟机设置更改网络配置,选择刚才配置的桥接模式 静态ip设置: 我用的ubuntu24桌…...
【JVM面试篇】高频八股汇总——类加载和类加载器
目录 1. 讲一下类加载过程? 2. Java创建对象的过程? 3. 对象的生命周期? 4. 类加载器有哪些? 5. 双亲委派模型的作用(好处)? 6. 讲一下类的加载和双亲委派原则? 7. 双亲委派模…...
【JVM】Java虚拟机(二)——垃圾回收
目录 一、如何判断对象可以回收 (一)引用计数法 (二)可达性分析算法 二、垃圾回收算法 (一)标记清除 (二)标记整理 (三)复制 (四ÿ…...
前端中slice和splic的区别
1. slice slice 用于从数组中提取一部分元素,返回一个新的数组。 特点: 不修改原数组:slice 不会改变原数组,而是返回一个新的数组。提取数组的部分:slice 会根据指定的开始索引和结束索引提取数组的一部分。不包含…...
【Elasticsearch】Elasticsearch 在大数据生态圈的地位 实践经验
Elasticsearch 在大数据生态圈的地位 & 实践经验 1.Elasticsearch 的优势1.1 Elasticsearch 解决的核心问题1.1.1 传统方案的短板1.1.2 Elasticsearch 的解决方案 1.2 与大数据组件的对比优势1.3 关键优势技术支撑1.4 Elasticsearch 的竞品1.4.1 全文搜索领域1.4.2 日志分析…...
