C++ 20 标准协程入门教程
基本概念 (是什么)
协程(coroutine): 是一种特殊的函数,其可以被暂停(suspend), 恢复执行(resume)。一个协程可
以被多次调用。
协程(coroutine): 分为stackless和stackful两种,所谓stackless协程是指协程被suspend时不
需要堆栈,而stackful协程被suspend时需要堆栈。C++中的协程属于stackless协程。
C++20开始引入的协程由于如下等原因难以学习
围绕协程实现的相应组件多(譬如co_wait, co_return, co_yield, promise,handle等组件)
灵活性高,有些组件提供的接口多,可由用户自己控制相应的实现
组件之间的关系也略复杂
编译器内部转换
首先,实现自己的第一个协程
体验协程
C++20与协程相关的组件包括
协程接口(也即协程返回类)
promise对象
coroutine handle
awaitable 和 awaiter
以及相应的三个关键字 co_wait, co_return, co_yield
下面看一个实际的hello协程的例子,其代码实现为
// coro_task.h
#ifndef COROUTINE_CORO_TASK_H
#define COROUTINE_CORO_TASK_H
#include <coroutine>
#include <exception>
#include <type_traits>
class Task
{
public:
struct promise_type;
using TaskHd1 = std::coroutine_handle<promise_type>;
private:
TaskHd1 hd1_;
public:
Task(auto h) : hd1_{h} {}
~Task() {
if (hd1_) { hd1_.destroy(); }
}
Task(const Task&) = delete;
Task& operator=(const Task&) = delete;
bool resume() {
if (!hd1_ || hd1_.done())
return false;
hd1_.resume();
return true;
}
public:
struct promise_type {
/* data */
auto get_return_object() {
return Task{TaskHd1::from_promise(*this)};
}
auto initial_suspend() { return std::suspend_always{};}
void unhandled_exception() { std::terminate();}
void return_void() {}
auto final_suspend() noexcept { return std::suspend_always{}; }
};
};
#endif //COROUTINE_CORO_TASK_H
// main.cpp
#include <iostream>
#include "coro_task.h"
Task hello(int max) {
std::cout << "hello world\n";
for (int i = 0; i < max; ++i) {
std::cout << "hello " << i << "\n";
co_await std::suspend_always{};
}
std::cout << "hello end\n";
}
int main() {
auto co = hello(3);
while (co.resume()) {
std::cout << "hello coroutine suspend\n";
}
return 0;
}
编译器构建命令为
# g++ -o main main.cc coro_task.h -std=c++20
{CMAKE_PATH}\cmake.exe --build {PROJECT_PATH}\build --target Coroutine -j 10
运行结果如下
hello world
hello 0
hello coroutine suspend
hello 1
hello coroutine suspend
hello 2
hello coroutine suspend
hello end
hello coroutine suspend
从总体上来看,协程和调用者之间的关系可由下图表示

在图中,可以把协程看作生产者,调用者看作消费者,把协程的返回值(协程接口,看作泛管道)。
此外图中出现的编程人员主要负责根据c++标准所提供的接口,可自定义泛管道的一些行为,以控制协程进行不同的操作。
若要实现一个协程,需要首先提供一个协程接口,譬如Task,在协程接口中需要提供
\(promise\_type\)
\(std::coroutine\_handle<promise\_type>\)
在协程的函数体中需要使用 co_await,co_yield, co_return 之一的关键词。
hello协程工作过程
此部分主要讲解hello协程的工作过程
- a. 协程的调用同函数相同,在main函数中,通过如下形式调用协程
auto co = hello(3);
b. 通过调用hello(3), 启动协程,协程立即暂停(suspend), 并返回协程接口Task对象给调用者
c. 在main函数中,调用co实例的resume接口,通过coroutine handle恢复协程的执行
d. 在协程中,进入for循环,初始化局部变量,并到达暂停点(suspend point), 暂停点由co_await expr确定
e. 协程暂停后将控制权转移到main函数,main函数继续运行并从新恢复协程
f. 协程恢复执行,继续for循环,i的值增加。再一次到达暂停点(suspend point)。转换控制权到main函数。
g. 最终for循环结束,协程离开for循环。并将控制权返回到到main函数,main函数退出循环,并销毁协程。
promise_type
promise_type可以用来控制协程的行为。详细来说,promise_type给开发者提供了如下能力
与 coroutine 交互(从coroutine接受消息,以及将消息发送给coroutine)
创建 coroutine handle
控制 coroutine 的 suspend 时机(在coroutine的入口和结尾)
提供了异常处理
一个promise_type有如下接口
struct promise_type
{
coroutine get_return_object() { return {coroutine::from_promise(*this)}; }
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
auto yeid_value(val);
auto return_value(val);
auto await_transform(...);
operator new(sz);
operator delete(ptr, sz);
auto get_return_object_on_allocation_failure();
};
一个类(模版类)只要实现了promise type所需要的接口,那么该类便可以用作协程的promise_type。一个简单的模版promise_type类如下
#include <coroutine>
#include <exception>
template <typename Type>
struct CoroPromise {
auto get_return_object() {
return std::coroutine_handle<CoroPromise<Type>>::from_promise(*this);
}
auto initial_suspend() { return std::suspend_always{}; }
void unhandled_exception() { std::terminate(); }
void return_void() {}
auto final_suspend() noexcept { return std::suspend_always{}; }
};
上述接口说明如下
get_return_object(): 主要用来初始化协程接口,用来作为协程的返回值。 该函数一般做两件事情
创建coroutine handle. 通过std::coroutine_handle的静态成员函数from_promise创建和初始化
利用创建的coroutine handle初始化协程接口
initial_suspend(): 定制coroutine启动时的行为。 eagerly 或者 lazily。
当返回值为std::suspend_never{},coroutine 立马执行(eagerly)
当返回值为std::suspend_always{},coroutine 立马暂停(suspending, lazily)
return_void(): 定制coroutine到达结尾或者碰到co_return 语句时的行为。当promise_type声明并定义了该成员函数,那么coroutine必须不能返回任何值。
unhandled_exception(): 定制coroutine处理异常的方式。可以重新抛出异常也可以直接调用std::terminate()
final_suspend(): 定制coroutine是否被最终suspend。这个函数需要保证不能抛出异常,且应该总是返回std::suspend_always{}
coroutine_handle
coroutine_handle表示一个正在执行或者暂停(suspend)的coroutine。在C++20中,coroutine_handle由标准模版
std::coroutine_handle<>表示。 std::coroutine_handle的模版参数为promise_type的类型。
目前C++20针对std::coroutine_handle提供了三种版本,分别为
template <>
struct std::coroutine_handle<void>;
template <typename Promise = void>
struct std::coroutine_handle;
template<>
struct coroutine_handle<std::noop_coroutine_promise>;
struct std::coroutine_handle; 可以存储任何种类的协程
struct std::coroutine_handle; 存储由Promise制定的协程
struct coroutine_handlestd::noop_coroutine_promise; 存储一个no-op的协程。
当你调用一个coroutine时,编译器会为你创建一个coroutine frame。该coroutine frame中会存放coroutine相应的状态。 为了恢复coroutine的执行或者销毁coroutine frame,你需要一个coroutine handle,其用来定位相应的coroutine frame。
C++20中的上述三个coroutine_handle的接口声明如下
template <>
struct coroutine_handle<void> {
public:
// [coroutine.handle.con], construct/reset
constexpr coroutine_handle() noexcept;
constexpr coroutine_handle(std::nullptr_t __h) noexcept;
coroutine_handle& operator=(std::nullptr_t) noexcept;
public:
// [coroutine.handle.export.import], export/import
constexpr void* address() const noexcept;
constexpr static coroutine_handle from_address(void* __a) noexcept;
// [coroutine.handle.observers], observers
constexpr explicit operator bool() const noexcept;
bool done() const noexcept;
// [coroutine.handle.resumption], resumption
void operator()() const;
void resume() const;
void destroy() const;
};
template <typename _Promise>
struct coroutine_handle {
// [coroutine.handle.con], construct/reset
constexpr coroutine_handle() noexcept;
constexpr coroutine_handle(nullptr_t) noexcept;
static coroutine_handle from_promise(_Promise& __p);
coroutine_handle& operator=(nullptr_t) noexcept;
// [coroutine.handle.export.import], export/import
constexpr void* address() const noexcept;
constexpr static coroutine_handle from_address(void* __a) noexcept;
// [coroutine.handle.conv], conversion
constexpr operator coroutine_handle<>() const noexcept;
// [coroutine.handle.observers], observers
constexpr explicit operator bool() const noexcept;
bool done() const noexcept;
// [coroutine.handle.resumption], resumption
void operator()() const;
void resume() const;
void destroy() const;
// [coroutine.handle.promise], promise access
_Promise& promise() const;
};
/// [coroutine.noop]
struct noop_coroutine_promise {};
// 17.12.4.1 Class noop_coroutine_promise
/// [coroutine.promise.noop]
template <>
struct coroutine_handle<noop_coroutine_promise> {
// _GLIBCXX_RESOLVE_LIB_DEFECTS
// 3460. Unimplementable noop_coroutine_handle guarantees
// [coroutine.handle.noop.conv], conversion
constexpr operator coroutine_handle<>() const noexcept;
// [coroutine.handle.noop.observers], observers
constexpr explicit operator bool() const noexcept { return true; }
constexpr bool done() const noexcept { return false; }
// [coroutine.handle.noop.resumption], resumption
void operator()() const noexcept {}
void resume() const noexcept {}
void destroy() const noexcept {}
// [coroutine.handle.noop.promise], promise access
noop_coroutine_promise& promise() const noexcept;
// [coroutine.handle.noop.address], address
constexpr void* address() const noexcept;
private:
friend coroutine_handle noop_coroutine() noexcept;
explicit coroutine_handle() noexcept = default;
}
对于std::coroutine_handle而言,其关键的接口为
address() 产生一个coroutine内部data的地址
from_address() 根据promise type的地址生成相应的std::coroutine_handle
operator bool() 查询一个coroutine handle所共享的coroutine frame为空
done() 用来查询一个coroutine在final_suspend点是否是暂停的(suspend)
resume() 以及 operator()() 恢复coroutine的执行
destory() 销毁coroutine frame
对于std::oroutine_handle而言,其与std::coroutine_handle相比较而言多了如下接口
from_promise() 根据传入的PromiseType对象 构建std::coroutine_handle
operator coroutine_handle<>() 将std::coroutine_handle转换为std::coroutine_handle
promise() 返回std::coroutine_handle所相关的PromiseType实例
coroutine接口
所谓coroutine接口,也即是coroutine返回给coroutine调用者的实例,用来与coroutine调用者进行通信的媒介。
若要通过coroutine接口来定制coroutine的行为,需要在coroutine接口中实现promise_type以及定义std::coroutine_handle成员。
简单实践
若想将coroutine中的一些信息返回给coroutine调用者,可以初步使用co_yield和co_return这两个关键字。
首先给出一个示例,co_yield val,并在coroutine调用者,获取该val值的代码
#include <iostream>
#include <coroutine>
#include <string>
class CoroTask {
public:
struct promise_type;
using CoroHd = std::coroutine_handle<promise_type>;
CoroTask(auto hd) : hd_{hd} {}
~CoroTask() {
if (hd_) { hd_.destroy(); }
}
CoroTask(const CoroTask&) = delete;
CoroTask& operator=(const CoroTask&) = delete;
int getVlaue() const { return hd_.promise().CoroValue; }
bool resume() {
if (hd_ && !hd_.done()) {
hd_.resume();
return true;
}
return false;
}
public:
struct promise_type {
int CoroValue{};
/* data */
auto get_return_object() {
return CoroTask{CoroHd::from_promise(*this)};
}
auto initial_suspend() { return std::suspend_always{};}
void unhandled_exception() { std::terminate();}
auto yield_value(int val) {
CoroValue = val;
return std::suspend_always{};
}
void return_void() {}
auto final_suspend() noexcept { return std::suspend_always{}; }
};
private:
CoroHd hd_;
};
#include "coro_task.h"
CoroTask coro(int max) {
std::cout << "coro start, max: " << max << "\n";
for (int i = 0; i <= max; ++i) {
std::cout << "coro index: " << i << "\n";
co_yield i;
}
std::cout << "coro end\n";
}
int main() {
auto task = coro(4);
while (task.resume()) {
std::cout << "main get coroutine value: " << task.getVlaue() << "\n";
}
return 0;
}
该示例代码的输出结果如下所示
coro start, max: 4
coro index: 0
main get coroutine value: 0
coro index: 1
main get coroutine value: 1
coro index: 2
main get coroutine value: 2
coro index: 3
main get coroutine value: 3
coro index: 4
main get coroutine value: 4
coro end
main get coroutine value: 4
若要使用co_yield将coroutine中的某些信息传递给coroutine调用者需要coder做如下工作
在promise type类中声明并定义所需要的信息结构
在promise type类中声明并定义yield_value接口,并将该接口的参数设置为相应的信息结构的类型
在yield_value的实现中完成信息的写入
在coroutine接口(coroutine返回值)中提供一个获取信息结构的接口,在该接口的实现中通过std::coroutine_handle的promise()接口获取coroutine相关的promise type并取回相应的值。
为了在coroutine返回值 CoroTask上使用range for,可以对CoroTask中增加begin()和end()接口,用来遍历coroutine中co_yield所投递的消息。
此外,为了提供begin()和end()接口,需要在CoroTask中提供iterator类,该类一般需要
重载操作符,也即operator()
重载++操作符,也即operator++()
重载操作符,也即operator()
因此一个最简单的iterator实现为
struct iterator {
CoroHd hd_{};
iterator(auto hd) : hd_(hd) {}
void getNext() {
if (!hd_ || hd_.done()) { return; }
hd_.resume();
if (hd_.done()) { hd_ = nullptr; }
}
int operator*() const {
return hd_.promise().CoroValue;
}
iterator operator++() {
getNext();
return *this;
}
bool operator==(const iterator&) const = default;
};
将其整合到CoroTask中,最终的CoroTask接口实现如下
#include <iostream>
#include <coroutine>
#include <string>
class CoroTask {
public:
struct promise_type;
using CoroHd = std::coroutine_handle<promise_type>;
CoroTask(auto hd) : hd_{hd} {}
~CoroTask() {
if (hd_) { hd_.destroy(); }
}
CoroTask(const CoroTask&) = delete;
CoroTask& operator=(const CoroTask&) = delete;
int getVlaue() { return hd_.promise().CoroValue; }
bool resume() {
if (hd_ && !hd_.done()) {
hd_.resume();
return true;
}
return false;
}
public:
struct promise_type {
int CoroValue{};
/* data */
auto get_return_object() {
return CoroTask{CoroHd::from_promise(*this)};
}
auto initial_suspend() { return std::suspend_always{};}
void unhandled_exception() { std::terminate();}
auto yield_value(int val) {
CoroValue = val;
return std::suspend_always{};
}
void return_void() {}
auto final_suspend() noexcept { return std::suspend_always{}; }
};
private:
struct iterator {
CoroHd hd_{};
iterator(auto hd) : hd_(hd) {}
void getNext() {
if (!hd_ || hd_.done()) { return; }
hd_.resume();
if (hd_.done()) { hd_ = nullptr; }
}
int operator*() const {
return hd_.promise().CoroValue;
}
iterator operator++() {
getNext();
return *this;
}
bool operator==(const iterator&) const = default;
};
public:
iterator begin() const {
if (!hd_ || hd_.done()) {
return iterator{nullptr};
}
iterator it{hd_};
it.getNext();
return it;
}
iterator end() const {
return iterator{nullptr};
}
private:
CoroHd hd_;
};
此时更改main函数中的语句,将其整体修正如下
int main() {
auto task = coro(4);
for (const auto& val : task) {
std::cout << "main get coroutine value: " << val << "\n";
}
return 0;
}
上述输出为
coro start, max: 4
coro index: 0
main get coroutine value: 0
coro index: 1
main get coroutine value: 1
coro index: 2
main get coroutine value: 2
coro index: 3
main get coroutine value: 3
coro index: 4
main get coroutine value: 4
coro end
利用co_return返回信息给coroutine调用者
再这里将coroutine中产生的vector返回给coroutine调用者,并在coroutine调用者中输出相应的内容。
co_return的使用可参考如下示例
template <typename T>
class CoGen {
public:
class promise_type {
public:
CoGen get_return_object() {
return CoGen{std::coroutine_handle<promise_type>::from_promise(*this)};
}
void return_value(const std::vector<T>& vec) {
vec_ = vec;
}
auto initial_suspend() { return std::suspend_always{};}
void unhandled_exception() { std::terminate();}
auto final_suspend() noexcept { return std::suspend_always{}; }
const std::vector<T>& getResult() const { return vec_; }
private:
std::vector<T> vec_{};
};
CoGen(auto hd) : hd_{hd} {}
~CoGen() {
if (hd_) {
hd_.destroy();
}
}
std::vector<T> getResult() const { return hd_.promise().getResult(); }
bool resume() {
if (!hd_ || hd_.done()) return false;
hd_.resume();
return true;
}
private:
std::coroutine_handle<promise_type> hd_{};
};
CoGen<int> coroGen(int max) {
std::vector<int> vec;
vec.resize(abs(max));
std::iota(vec.begin(), vec.end(), 0);
co_return vec;
}
int main() {
auto task = coroGen(7);
while (task.resume());
std::cout << "coroutine end\n";
for (const auto& val : task.getResult()) {
std::cout << " " << val;
}
std::cout << "\n";
return 0;
}
上述输出结果为
coroutine end
0 1 2 3 4 5 6
利用co_return返回信息给coroutine调用者需要在promise type中进行如下工作
声明并定义return_value(params)接口
声明并定义相应的信息结构
在return_value中完成信息的输入
提供某些接口供coroutine接口使用
总结
基础部分讲解了C++20中coroutine的基本概念,stackless coroutine以及stackful coroutine。
C++20 coroutine是编程不友好的,主要由于其组件多,依赖复杂,编译做的转换操作多。为了体验协程,本部分讲解了
co_await
co_yield
co_return
这几个关键字的使用。
同时也讲解了promise type类型的概念和作用; std::coroutine_handle的接口和作用。
本部分较为基础。下一部分将深入coroutine。包括但不限于
co_await的具体工作原理
为什么需要在coroutine接口中定义promise_type, 可否不在coroutine接口中定义promise_type
promise type是如何控制coroutine的行为
C++ 20 标准协程入门教程的更多相关文章
- 比物理线程都好用的C++20的协程,你会用吗?
摘要:事件驱动(event driven)是一种常见的代码模型,其通常会有一个主循环(mainloop)不断的从队列中接收事件,然后分发给相应的函数/模块处理.常见使用事件驱动模型的软件包括图形用户界 ...
- PHP协程入门详解
概念 咱们知道多进程和多线程是实现并发的有效方式.但多进程的上下文切换资源开销太大:多线程开销相比要小很多,也是现在主流的做法,但其的控制权在内核,从而使用户(程序员)失去了对代码的控制,而且线程的上 ...
- (20)gevent协程
协程: 也叫纤程,协程是线程的一种实现,指的是一条线程能够在多任务之间来回切换的一 种实现,对于CPU.操作系统来说,协程并不存在 任务之间的切换会花费时间.目前电脑配置一般线程开到200会阻塞卡顿 ...
- Python Web学习笔记之Python多线程和多进程、协程入门
进程和线程究竟是什么?如何使用进程和线程?什么场景下需要使用进程和线程?协程又是什么?协程和线程的关系和区别有哪些? 程序切换-CPU时间的分配 首先,我们的任何一个程序都需要运行在一个操作系统中,如 ...
- Kotlin协程入门
开发环境 IntelliJ IDEA 2021.2.2 (Community Edition) Kotlin: 212-1.5.10-release-IJ5284.40 介绍Kotlin中的协程.用一 ...
- Android Kotlin协程入门
Android官方推荐使用协程来处理异步问题.以下是协程的特点: 轻量:单个线程上可运行多个协程.协程支持挂起,不会使正在运行协程的线程阻塞.挂起比阻塞节省内存,且支持多个并行操作. 内存泄漏更少:使 ...
- cocos creator主程入门教程(二)—— 弹窗管理
五邑隐侠,本名关健昌,10年游戏生涯,现隐居五邑.本系列文章以TypeScript为介绍语言. 我们已经知道怎样制作.加载.显示界面.但cocos没有提供一个弹窗管理模块,对于一个多人合作的项目,没有 ...
- 20.multi_协程方法抓取总阅读量
# 用asyncio和aiohttp抓取博客的总阅读量 (提示:先用接又找到每篇文章的链接) # https://www.jianshu.com/u/130f76596b02 import re im ...
- cocos creator主程入门教程(四)—— 网络通信
五邑隐侠,本名关健昌,10年游戏生涯,现隐居五邑.本系列文章以TypeScript为介绍语言. 前面已经介绍怎样加载资源.管理弹窗.开发一个网络游戏,难免要处理网络通信.有几点问题需要注意: 1.服务 ...
- cocos creator主程入门教程(七)—— MVC架构
五邑隐侠,本名关健昌,10年游戏生涯,现隐居五邑.本系列文章以TypeScript为介绍语言. 这一篇将介绍在游戏客户端常用的架构MVC架构.一个游戏的MVC如下划分: M:1)单例全局的数据中心Wo ...
随机推荐
- Util应用框架基础(七) - 缓存
本节介绍Util应用框架如何操作缓存. 概述 缓存是提升性能的关键手段之一. 除了提升性能,缓存对系统健壮性和安全性也有影响. 不同类型的系统对缓存的依赖程度不同. 对于后台管理系统,由于是给管理人员 ...
- .NET周刊【11月第2期 2023-11-12】
国内文章 一个基于百度飞桨封装的.NET版本OCR工具类库 - PaddleOCRSharp https://www.cnblogs.com/Can-daydayup/p/17818557.html ...
- Shell必备三剑客
Top 目录 Sed--三剑客之一 基本格式 选项及含义 命令flags标记及功能 支持正则表达式, 扩展正则表达式 高级命令 命令格式 注意: 命令示例 字符串替换----'s' 行内容替换--'c ...
- 重写Nacos服务发现逻辑动态修改远程服务IP地址
背景 还是先说下做这个的背景,开发环境上了K8S,所有的微服务都注册在K8S内的Nacos,注册地址为K8S内部虚拟IP,K8S内的服务之间相互调用没有问题,但是本机开发联调调用其他微服务就访问不到. ...
- python3使用pandas备份mysql数据表
操作系统 :CentOS 7.6_x64 Python版本:3.9.12 MySQL版本:5.7.38 日常开发过程中,会遇到mysql数据表的备份需求,需要针对单独的数据表进行备份并定时清理数据. ...
- 我用 AI 写的《JavaScript 工程师的 Python 指南》电子书发布啦!
关于本书 你好,我是 luckrnx09,一名靠 React 恰饭的前端工程师,很高兴向你介绍我的第一本开源电子书<JavaScript 工程师的 Python 指南>. 本书的内容完全免 ...
- SpringBoot发送虚拟请求~
1.创建一个测试用的TestController @RestController public class TestController { @GetMapping("/test" ...
- GPT-4多模态大型语言模型发布
GPT-4 模型是OpenAI开发的第四代大型语言模型(LLM),它将是一个多模态模型,会提供完全不同的可能性-例如文字转图像.音乐甚至视频.GPT 全称为 Generative Pre-traine ...
- Shell脚本实践总结
对比大小 符号用法:(必须使用双括号) < 小于 (( "$a" < "$b" )) <= 小于等于 (( "$a&q ...
- android学习笔记(1)
Android 开发框架 android系统是一个开放且体积庞大的系统,从功能上,将android开发分为移植开发移动电话系统,android应用开发和android系统开发三种. 移动移植移动电话系 ...