C++20 协程

C++ 20有一个新玩意,协程。这玩意对C++的未来可能是重要一环,也会是让C++成为服务器编程有力工具。

对C++20的协程,最简单的理解协程是可以重入的特殊函数。就是这个函数在执行的过程,可以(通过co_await ,或者co_yield)挂起,然后在外部(通过coroutine_handle)恢复运行。

我测试的代码都是在Visual studio 2022上运行的。据说GCC 10.0 也已经支持。

协程是特殊的函数

首先再次强调,C++ 20的协程是一个特殊函数。只是这个函数具有挂起和恢复的能力,可以被挂起(挂起后调用代码继续向后执行),而后可以继续恢复其执行。如下图:

如图所示,协程并没有一次执行完成,可以被反复挂起,挂起后可以恢复到挂起的点继续运行。

C++ 20协程的特点

那我们来看看C++ 20 协程的一些特点和用途。

首先,C++ 20 协程是一个无栈(stackless)的协程。同时,C++ 20 协程是非对称的协程,和Linux传统的Context Switch有区别。更像Windows的纤程。和C#的协程也比较相像,毕竟是微软的提案。

传统的Context Switch是有栈协程,你可以认为Context 协程都是运行在栈上,Context 协程的切换就是切换栈。同时因为其是有栈协程。切换是对称的,都是栈切换。你可以从主线程上切换为另外一个Context 协程栈,也可以从一个Context 协程切换为主线程,也可以Context 协程之间切换。Context 协程的状态也就是保存在栈上。

C++ 20的协程可以用来干啥呢?和大部分协程用途类似,就是异步编程用的。看图1就可以明白,每次一次协程的挂起都可以视为协程进入一个等待状态,比如请求一个网络,需要HTTP get一个文件,然后对文件进行分析。那么就可以用协程来包装整个处理,在发起HTTP请求后,挂起协程(处理其他事情),等待应答或者超时后,再恢复协程的运行。

但不足的是目前C++ 20的协程才是一个开始,说实话,目前的协程只提供基本框架,写起来并不舒服。C++目前在IO方面,特别是网络IO方面还不完善。需要一个大量异步IO库,才能用好C++ 20协程。

如果C++ 20的协程周边更加完整,也许C++又能在服务器编程这块能重新面对Go这类语言的威胁。

C++协程的是三个关键字

C++的协程(协程函数)内部可以用co_await , co_yield.两个关键字挂起协程,co_return,关键字进行返回。

co_await

co_await调用一个awaiter对象(可以认为是一个接口),根据其内部定义决定其操作是挂起,还是继续,以及挂起,恢复时的行为。其呈现形式为

cw_ret = co_await  awaiter;

cw_ret 记录调用的返回值,其是awaiter的await_resume 接口返回值。

co_await 相对比较复杂,后面开一章详细讲。

co_yield

挂起协程。其出现形式是

co_yield  cy_ret;

cy_ret会保存在promise承诺对象中(通过yield_value函数)。在协程外部可以通过promise得到。

co_return

协程返回。其出现形式是

co_return cr_ret;

cr_ret会保存在promise承诺对象中(通过return_value函数)。在协程外部可以通过promise得到。要注意,cr_ret并不是协程的返回值。这个是有区别的。

C++协程的重要概念

C++ 的编译器如何识别协程函数呢?是通过函数返回值。C++ 协程函数的返回类型有要求,返回类型是result ,而result里面必须有一个子类型承诺对象(promise),呈现为Result::promise_type。承诺对象(promise)是一个接口,里面实现get_return_object等接口。而通过std::coroutine_handle<promise_type>::from_promise( promise& p )这个静态函数,我们可以得到协程句柄(coroutine handle)。而协程的运行状态 ,协程函数的形参,内部变量,临时变量,挂起暂停在什么点,被保存在协程状态 (coroutine state)中。

好了上面的描述,我们可以看出协程的几个重要概念。

  • 协程状态 (coroutine state),记录协程状态,是分配于堆的内部对象:

承诺对象

形参(协程函数的参数)

协程挂起的点

临时变量

  • 承诺对象(promise),从协程内部操纵。协程通过此对象提交其结果或异常。
  • 协程句柄(coroutine handle),协程的唯一标示。用于恢复协程执行或销毁协程帧。
  • 等待体(awaiter),co_await 关键字调用的对象。

协程状态(coroutine state)

协程状态(coroutine state)是协程启动开始时,new空间存放协程状态,协程状态记录协程函数的参数,协程的运行状态,变量。挂起时的断点。

注意,协程状态 (coroutine state)并不是就是协程函数的返回值RET。虽然我们设计的RET一般里面也有promise和coroutine handle,大家一般也是通过RET去操作协程的恢复,获取返回值。但coroutine state理论上还应该包含协程运行参数,断点等信息。而协程状态 (coroutine state)应该是协程句柄(coroutine handle)对应的一个数据,而由系统管理的。

承诺对象(promise)

承诺对象的表现形式必须是result::promise_type,result为协程函数的返回值。

承诺对象是一个实现若干接口,用于辅助协程,构造协程函数返回值;提交传递co_yield,co_return的返回值。明确协程启动阶段是否立即挂起;以及协程内部发生异常时的处理方式。其接口包括:

  • auto get_return_object() :用于生成协程函数的返回对象。
  • auto initial_suspend():用于明确初始化后,协程函数的执行行为,返回值为等待体(awaiter),用co_wait调用其返回值。返回值为std::suspend_always 表示协程启动后立即挂起(不执行第一行协程函数的代码),返回std::suspend_never 表示协程启动后不立即挂起。(当然既然是返回等待体,你可以自己在这儿选择进行什么等待操作)
  • void return_value(T v):调用co_return v后会调用这个函数,可以保存co_return的结果
  • auto yield_value(T v):调用co_yield后会调用这个函数,可以保存co_yield的结果,其返回其返回值为std::suspend_always表示协程会挂起,如果返回std::suspend_never表示不挂起。
  • auto final_suspend() noexcept:在协程退出是调用的接口,返回std::suspend_never ,自动销毁 coroutine state 对象。若 final_suspend 返回 std::suspend_always 则需要用户自行调用 handle.destroy() 进行销毁。但值得注意的是返回std::suspend_always并不会挂起协程。

前面我们提到在协程创建的时候,会new协程状态(coroutine state)。你可以通过可以在 promise_type 中重载 operator new 和 operator delete,使用自己的内存分配接口。(请参考再探 C++20 协程)

协程句柄(coroutine handle)

协程句柄(coroutine handle)是一个协程的标示,用于操作协程恢复,销毁的句柄。

协程句柄的表现形式是std::coroutine_handle<promise_type>,其模板参数为承诺对象(promise)类型。句柄有几个重要函数:

  • resume()函数可以恢复协程。
  • done()函数可以判断协程是否已经完成。返回false标示协程还没有完成,还在挂起。

协程句柄和承诺对象之间是可以相互转化的。

  • std::coroutine_handle<promise_type>::from_promise : 这是一个静态函数,可以从承诺对象(promise)得到相应句柄。
  • std::coroutine_handle<promise_type>::promise() 函数可以从协程句柄coroutine handle得到对应的承诺对象(promise)

等待体(awaiter)

co_wait 关键字会调用一个等待体对象(awaiter)。这个对象内部也有3个接口。根据接口co_wait  决定进行什么操作。

  • bool await_ready():等待体是否准备好了,返回 false ,表示协程没有准备好,立即调用await_suspend。返回true,表示已经准备好了。
  • auto await_suspend(std::coroutine_handle<> handle)如果要挂起,调用的接口。其中handle参数就是调用等待体的协程,其返回值有3种可能

void 同返回true

bool 返回true 立即挂起,返回false 不挂起。

返回某个协程句柄(coroutine handle),立即恢复对应句柄的运行。

  • auto await_resume() :协程挂起后恢复时,调用的接口。返回值作为co_wait 操作的返回值。

等待体(awaiter)值得用更加详细的笔墨书写一章,我们就放一下,先了解其有2个特化类型。

  • std::suspend_never类,不挂起的的特化等待体类型。
  • std::suspend_always类,挂起的特化等待体类型。

前面不少接口已经用了这2个特化的类,同时也可以明白其实协程内部不少地方其实也在使用co_wait 关键字。

例子,“七进七出”的协程。

好了。所有概念我们介绍基本完成了。先来段代码吧。否则实在憋屈。

这个例子主要展现的是协程函数和主线程之间的切换。协程反复中断,然后在main函数内部又恢复其运行。直至最后co_return。

这个例子虽然简单,但如果你对异步编程有所了解也能明白如何利用C++20完成一段异步编程了。源代码获取地址请点击

下面例子中:

  • coro_ret<int> coroutine_7in7out() 就是协程函数。
  • coro_ret<int> c_r 就是协程的返回值。在后续,都是通过c_r和协程进行交互。
  • coro_ret<int>::promise_type 就是承诺对象
  • std::coroutine_handle<promise_type> 就是句柄。
#include <coroutine>
#include <iostream>
#include <stdexcept>
#include <thread>//!coro_ret 协程函数的返回值,内部定义promise_type,承诺对象
template <typename T>
struct coro_ret
{struct promise_type;using handle_type = std::coroutine_handle<promise_type>;//! 协程句柄handle_type coro_handle_;coro_ret(handle_type h): coro_handle_(h){}coro_ret(const coro_ret&) = delete;coro_ret(coro_ret&& s): coro_handle_(s.coro_){s.coro_handle_ = nullptr;}~coro_ret(){//!自行销毁if (coro_handle_)coro_handle_.destroy();}coro_ret& operator=(const coro_ret&) = delete;coro_ret& operator=(coro_ret&& s){coro_handle_ = s.coro_handle_;s.coro_handle_ = nullptr;return *this;}//!恢复协程,返回是否结束bool move_next(){coro_handle_.resume();return coro_handle_.done();}//!通过promise获取数据,返回值T get(){return coro_handle_.promise().return_data_;}//!promise_type就是承诺对象,承诺对象用于协程内外交流struct promise_type{promise_type() = default;~promise_type() = default;//!生成协程返回值auto get_return_object(){return coro_ret<T>{handle_type::from_promise(*this)};}//! 注意这个函数,返回的就是awaiter//! 如果返回std::suspend_never{},就不挂起,//! 返回std::suspend_always{} 挂起//! 当然你也可以返回其他awaiterauto initial_suspend(){//return std::suspend_never{};return std::suspend_always{};}//!co_return 后这个函数会被调用void return_value(T v){return_data_ = v;return;}//!auto yield_value(T v){std::cout << "yield_value invoked." << std::endl;return_data_ = v;return std::suspend_always{};}//! 在协程最后退出后调用的接口。//! 若 final_suspend 返回 std::suspend_always 则需要用户自行调用//! handle.destroy() 进行销毁,但注意final_suspend被调用时协程已经结束//! 返回std::suspend_always并不会挂起协程(实测 VSC++ 2022)auto final_suspend() noexcept{std::cout << "final_suspend invoked." << std::endl;return std::suspend_always{};}//void unhandled_exception(){std::exit(1);}//返回值T return_data_;};
};//这就是一个协程函数
coro_ret<int> coroutine_7in7out()
{//进入协程看initial_suspend,返回std::suspend_always{};会有一次挂起std::cout << "Coroutine co_await std::suspend_never" << std::endl;//co_await std::suspend_never{} 不会挂起co_await std::suspend_never{};std::cout << "Coroutine co_await std::suspend_always" << std::endl;co_await std::suspend_always{};std::cout << "Coroutine stage 1 ,co_yield" << std::endl;co_yield 101;std::cout << "Coroutine stage 2 ,co_yield" << std::endl;co_yield 202;std::cout << "Coroutine stage 3 ,co_yield" << std::endl;co_yield 303;std::cout << "Coroutine stage end, co_return" << std::endl;co_return 808;
}int main(int argc, char* argv[])
{bool done = false;std::cout << "Start coroutine_7in7out ()\n";//调用协程,得到返回值c_r,后面使用这个返回值来管理协程。auto c_r = coroutine_7in7out();//第一次停止因为initial_suspend 返回的是suspend_always//此时没有进入Stage 1std::cout << "Coroutine " << (done ? "is done " : "isn't done ")<< "ret =" << c_r.get() << std::endl;done = c_r.move_next();//此时是,co_await std::suspend_always{}std::cout << "Coroutine " << (done ? "is done " : "isn't done ")<< "ret =" << c_r.get() << std::endl;done = c_r.move_next();//此时打印Stage 1std::cout << "Coroutine " << (done ? "is done " : "isn't done ")<< "ret =" << c_r.get() << std::endl;done = c_r.move_next();std::cout << "Coroutine " << (done ? "is done " : "isn't done ")<< "ret =" << c_r.get() << std::endl;done = c_r.move_next();std::cout << "Coroutine " << (done ? "is done " : "isn't done ")<< "ret =" << c_r.get() << std::endl;done = c_r.move_next();std::cout << "Coroutine " << (done ? "is done " : "isn't done ")<< "ret =" << c_r.get() << std::endl;return 0;
}

本章总结

C++ 20 的协程是一个用于异步模型,主要用于服务开发的东东,由于概念和传统的同步变成有区别,同时自身的概念,接口,关键字很多。理解起来有点难度。

  • C++ 20的协程是一个特殊的可以挂起,恢复(重入)的函数。
  • 协程内部可以出现三个关键字。co_await awaiter用于和等待体交互,可以挂起和恢复。co_yield 用于挂起协程,co_return用于返回。
  • 编译器通过返回值(类型result)里面result::promise(承诺对象)来识别什么函数是协程函数。承诺对象(promise)是用于协程和外部代码做交互。
  • 系统在进入协程时,会new构建一个协程状态(coroutine state),协程状态(coroutine state)里面保存协程的参数,运行(暂停)的位置,临时变量,承诺对象等。
  • 每个协程都有一个协程句柄(coroutine handle)标示。协程句柄(coroutine handle)可以和承诺对象直接相互转化。在协程外部,可以用协程句柄恢复(resume)已经挂起的协程。

如果你对协程的各种接口调用顺序有疑惑,可以自己调试一下,比看文字记忆深刻。如果您没有耐心也可以看看第三章的文字。

参考文档

初探 C++20 协程

再探 C++20 协程,这两篇文字都不错。

Coroutines (C++20)

协程(coroutine)简介

The Coroutine in C++ 20 协程之诺

C++ 20 协程 Coroutine相关推荐

  1. C++20 协程coroutine

    1.协程概念 协程函数与普通函数的区别: (1)普通函数执行完返回,则结束. 协程函数可以运行到一半,返回并保留上下文:下次唤醒时恢复上下文,可以接着执行. 协程与多线程: (1)协程适合IO密集型程 ...

  2. C++20中的协程(Coroutine)

    目录 C++20中的协程(Coroutine) 那么,什么是协程? 那么这么好用的协程,是不是只要C++20一推出,我们加上一个关键字就能直接把异步调用转化为同步调用呢? 协程函数和Awaitable ...

  3. 并发编程协程(Coroutine)之Gevent

    并发编程协程之Gevent Gevent官网文档地址:http://www.gevent.org/contents.html 基本概念 我们通常所说的协程Coroutine其实是corporate r ...

  4. c++ 协程_理解Python协程(Coroutine)

    由于GIL的存在,导致Python多线程性能甚至比单线程更糟. GIL: 全局解释器锁(英语:Global Interpreter Lock,缩写GIL),是计算机程序设计语言解释器用于同步线程的一种 ...

  5. Unity 协程Coroutine综合测试

    Unity 协程Coroutine综合测试 1 using UnityEngine; 2 using System.Collections; 3 using System.Text; 4 5 publ ...

  6. qemu核心机制分析-协程coroutine

    关于协程coroutine前面的文章已经介绍过了,本文总结对qemu中coroutine机制的分析,qemu 协程coroutine基于:setcontext函数族以及函数间跳转函数siglongjm ...

  7. c++ 协程_Python3 协程(coroutine)介绍

    本文首发于 at7h 的个人博客. 目前 Python 语言的协程从实现来说可分为两类: 一种是基于传统生成器的协程,叫做 generator-based coroutines,通过包装 genera ...

  8. C++20协程初探!

    导语 | 本文推选自腾讯云开发者社区-[技思广益 · 腾讯技术人原创集]专栏.该专栏是腾讯云开发者社区为腾讯技术人与广泛开发者打造的分享交流窗口.栏目邀约腾讯技术人分享原创的技术积淀,与广泛开发者互启 ...

  9. 简单总结协程Coroutine及Yield常见用法

    原文地址:http://blog.csdn.net/qq_18995513/article/details/51944602 最近学习协程Coroutine,参考了别人的文章和视频教程,感觉协程用法还 ...

最新文章

  1. Selenium自动化测试驱动下载
  2. python之csv模块(part1)--写入csv文件
  3. cocos2dx游戏--欢欢英雄传说--添加攻击按钮
  4. OpenSSH 命令注入漏洞通告(CVE-2020-15778,Openssh <=8.3p1)
  5. 什么是可变变量以及如何使用
  6. 大家身边极度聪明的人是什么样子?
  7. AGC018C Coins (set)
  8. 【转】.Net 架构图
  9. 清理电脑文件夹中的Thumbs.db文件
  10. 一个小兔子的大数据见解1
  11. STM8L152的LCD模块原理及驱动
  12. java applet插件_Atitit.java的浏览器插件技术 Applet japplet attilax总结
  13. 3轴码垛机械臂运动学逆解
  14. 【openMV or openCV】
  15. PPI_DPI 对比
  16. php支付宝pc收银台,支付宝支付-PC电脑网站支付
  17. 那些裸辞的人后来都怎么样了?
  18. STM32H743ZI+MPU+LWIP ping解决
  19. Snipaste截图工具
  20. Cloud Token:一个趋势可以改变你的一生,有天你别后悔拍大腿……

热门文章

  1. php ejs,ejs模板
  2. 聚宽数据python_聚宽量化(Zen)
  3. EmEditor注册码及EmEditor注册机下载
  4. ECMAScript 6.0基础入门教程(一)-ES6基础入门教程
  5. 深度强化学习-基于价值学习的高级技巧(五-1)
  6. 浅析如何用Python进行中英文词频统计
  7. 百度前端技术学院html任务,百度前端技术学院(IFE)第一阶段任务
  8. python中forinrange_详细描述了如何在Python中使用for in range(range()函数的for循环),foriinrangerange,介绍...
  9. 程序员表白代码来了,太秀了。。
  10. PHP版本百度翻译有道云翻译讯飞云翻译api相关接口