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 ...
随机推荐
- 【scipy 基础】--稀疏矩阵
稀疏矩阵是一种特殊的矩阵,其非零元素数目远远少于零元素数目,并且非零元素分布没有规律.这种矩阵在实际应用中经常出现,例如在物理学.图形学和网络通信等领域. 稀疏矩阵其实也可以和一般的矩阵一样处理,之所 ...
- .net中优秀依赖注入框架Autofac看一篇就够了
Autofac 是一个功能丰富的 .NET 依赖注入容器,用于管理对象的生命周期.解决依赖关系以及进行属性注入.本文将详细讲解 Autofac 的使用方法,包括多种不同的注册方式,属性注入,以及如何使 ...
- localhost工具:本地代码的远程之路
在日常的开发过程中,本地代码远程调试一直是最理想的开发状态.本文通过介绍京东集团内开发的一个轻量简单的小工具"localhost",从多角度的方案思考,到原理介绍,到最终的方案落地 ...
- NLP复习之N元文法
N元文法的统计 二元概率方程: \[P(w_n|w_{n-1}) = \frac{C(w_{n-1}w_n)}{C(w_{n-1})} \] 三元概率估计方程: \[P(w_n|w_{n-2},w_{ ...
- int和String的相互转换
- linux云服务器病毒处理
阿里云服务器被挖矿病毒入侵,CPU跑满,需要先停止相关进程.为了根除病毒,还需要 解决系统的后门问题(这部分听从阿里云工程师的建议备份系统盘快照后重置系统,再通过快照恢复数据) 然而重置系统后依然存在 ...
- Go 自动补全gocode
go语言自动补全代码,需要添加gocode的程序. 执行: go get github.com/nsf/gocode 一般来说,gocode的源码会在$GOPATH/src/github.com/ns ...
- Programming abstractions in C阅读笔记:p184-p195
<Programming Abstractions In C>学习第61天,p184-p195总结. 一.技术总结 1.mutual recursion 2.natural number ...
- 使用bind搭建内网dns服务
dns服务端方案简介 dns服务有什么用呢,尤其是内网的dns服务,其实用处还蛮大的,我见过的典型使用,是数据库跨机房多活. 如某mysql主机搭建在深圳机房,为了保证高可用,那我们可以给这台主库,维 ...
- 在xml中比较运算符
SQL 中,可以使用比较运算符来比较两个值,如使用小于运算符 < 比较两个值大小.但是,在 SQL 查询中,有时候需要将小于运算符 < 用于 XML 或 HTML 语法中,这会导致语法冲突 ...