基本概念 (是什么)

协程(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_awaitco_yieldco_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 标准协程入门教程的更多相关文章

  1. 比物理线程都好用的C++20的协程,你会用吗?

    摘要:事件驱动(event driven)是一种常见的代码模型,其通常会有一个主循环(mainloop)不断的从队列中接收事件,然后分发给相应的函数/模块处理.常见使用事件驱动模型的软件包括图形用户界 ...

  2. PHP协程入门详解

    概念 咱们知道多进程和多线程是实现并发的有效方式.但多进程的上下文切换资源开销太大:多线程开销相比要小很多,也是现在主流的做法,但其的控制权在内核,从而使用户(程序员)失去了对代码的控制,而且线程的上 ...

  3. (20)gevent协程

    协程: 也叫纤程,协程是线程的一种实现,指的是一条线程能够在多任务之间来回切换的一 种实现,对于CPU.操作系统来说,协程并不存在 任务之间的切换会花费时间.目前电脑配置一般线程开到200会阻塞卡顿 ...

  4. Python Web学习笔记之Python多线程和多进程、协程入门

    进程和线程究竟是什么?如何使用进程和线程?什么场景下需要使用进程和线程?协程又是什么?协程和线程的关系和区别有哪些? 程序切换-CPU时间的分配 首先,我们的任何一个程序都需要运行在一个操作系统中,如 ...

  5. Kotlin协程入门

    开发环境 IntelliJ IDEA 2021.2.2 (Community Edition) Kotlin: 212-1.5.10-release-IJ5284.40 介绍Kotlin中的协程.用一 ...

  6. Android Kotlin协程入门

    Android官方推荐使用协程来处理异步问题.以下是协程的特点: 轻量:单个线程上可运行多个协程.协程支持挂起,不会使正在运行协程的线程阻塞.挂起比阻塞节省内存,且支持多个并行操作. 内存泄漏更少:使 ...

  7. cocos creator主程入门教程(二)—— 弹窗管理

    五邑隐侠,本名关健昌,10年游戏生涯,现隐居五邑.本系列文章以TypeScript为介绍语言. 我们已经知道怎样制作.加载.显示界面.但cocos没有提供一个弹窗管理模块,对于一个多人合作的项目,没有 ...

  8. 20.multi_协程方法抓取总阅读量

    # 用asyncio和aiohttp抓取博客的总阅读量 (提示:先用接又找到每篇文章的链接) # https://www.jianshu.com/u/130f76596b02 import re im ...

  9. cocos creator主程入门教程(四)—— 网络通信

    五邑隐侠,本名关健昌,10年游戏生涯,现隐居五邑.本系列文章以TypeScript为介绍语言. 前面已经介绍怎样加载资源.管理弹窗.开发一个网络游戏,难免要处理网络通信.有几点问题需要注意: 1.服务 ...

  10. cocos creator主程入门教程(七)—— MVC架构

    五邑隐侠,本名关健昌,10年游戏生涯,现隐居五邑.本系列文章以TypeScript为介绍语言. 这一篇将介绍在游戏客户端常用的架构MVC架构.一个游戏的MVC如下划分: M:1)单例全局的数据中心Wo ...

随机推荐

  1. 【scipy 基础】--稀疏矩阵

    稀疏矩阵是一种特殊的矩阵,其非零元素数目远远少于零元素数目,并且非零元素分布没有规律.这种矩阵在实际应用中经常出现,例如在物理学.图形学和网络通信等领域. 稀疏矩阵其实也可以和一般的矩阵一样处理,之所 ...

  2. .net中优秀依赖注入框架Autofac看一篇就够了

    Autofac 是一个功能丰富的 .NET 依赖注入容器,用于管理对象的生命周期.解决依赖关系以及进行属性注入.本文将详细讲解 Autofac 的使用方法,包括多种不同的注册方式,属性注入,以及如何使 ...

  3. localhost工具:本地代码的远程之路

    在日常的开发过程中,本地代码远程调试一直是最理想的开发状态.本文通过介绍京东集团内开发的一个轻量简单的小工具"localhost",从多角度的方案思考,到原理介绍,到最终的方案落地 ...

  4. 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_{ ...

  5. int和String的相互转换

  6. linux云服务器病毒处理

    阿里云服务器被挖矿病毒入侵,CPU跑满,需要先停止相关进程.为了根除病毒,还需要 解决系统的后门问题(这部分听从阿里云工程师的建议备份系统盘快照后重置系统,再通过快照恢复数据) 然而重置系统后依然存在 ...

  7. Go 自动补全gocode

    go语言自动补全代码,需要添加gocode的程序. 执行: go get github.com/nsf/gocode 一般来说,gocode的源码会在$GOPATH/src/github.com/ns ...

  8. Programming abstractions in C阅读笔记:p184-p195

    <Programming Abstractions In C>学习第61天,p184-p195总结. 一.技术总结 1.mutual recursion 2.natural number ...

  9. 使用bind搭建内网dns服务

    dns服务端方案简介 dns服务有什么用呢,尤其是内网的dns服务,其实用处还蛮大的,我见过的典型使用,是数据库跨机房多活. 如某mysql主机搭建在深圳机房,为了保证高可用,那我们可以给这台主库,维 ...

  10. 在xml中比较运算符

    SQL 中,可以使用比较运算符来比较两个值,如使用小于运算符 < 比较两个值大小.但是,在 SQL 查询中,有时候需要将小于运算符 < 用于 XML 或 HTML 语法中,这会导致语法冲突 ...