目录

  • C++20中的协程(Coroutine)
    • 那么,什么是协程?
    • 那么这么好用的协程,是不是只要C++20一推出,我们加上一个关键字就能直接把异步调用转化为同步调用呢?
    • 协程函数和Awaitable类
    • 那么,到底要如何使用协程呢?
    • 写在最后

C++20中的协程(Coroutine)

从2017年开始, 协程(Coroutine)的概念就开始被建议加入C++20的标准中了,并已经开始有人对C++20协程的提案进行了介绍。1事实上,协程的概念在很早就出现了,甚至其他语言(JS,Python,C#等)早就已经支持了协程。
可见,协程并不是C++所特有的概念。

那么,什么是协程?

简单来说,协程就是一种特殊的函数,它可以在函数执行到某个地方的时候暂停执行,返回给调用者或恢复者(可以有一个返回值),并允许随后从暂停的地方恢复继续执行。注意,这个暂停执行不是指将函数所在的线程暂停执行,而是单纯的暂停执行函数本身。

那么,这种特殊函数有什么用呢?最常见的用途,就是将“异步”风格的编程“同步”化。

比如,我们有一个请求webapi的库,然后在某个应用中我们需要发送一个http请求,然后等待web服务器反馈消息。恰巧的是,我们需要按顺序请求多次,比如,只有请求A返回了,我们才能发送请求B,因为请求B中包含请求A返回的结果。然后等请求B返回了,我们才能发送请求C等等。
我们不能阻塞主线程,那么此时我们应该怎么办?
最常见的思路就是开一个新线程,然后使用“回调函数”,例如:

// 示意代码
void requestA(int req, std::function<void(int)> cb)
{// 我们的webapi是异步调用, 我们开启一个线程请求并等待调用完毕std::thread t([req, cb]() {auto response = webapi.request(req);// 假定response有个等待返回值的接口waitForFinish,他会阻塞当前线程,直到拿到返回值int rt = response.waitForFinish();// 返回了, 那么我们调用回调函数cb(rt); });t.detach();
}

假定我们还有相同结构的requestB,requestC以及其它, 那么我们会怎么用呢? 有了lamda表达式,通过回调函数进行链式调用可以很简单的写成如下形式:

int main()
{requestA(1, [](int rt){requestB(rt, [](int rt2){requestC(rt2, [](int rt3){// 根据需要可能会继续嵌套下去});});});// 甚至可能需要再来一遍, 因为我们还需要使用另一个参数请求requestA(2, [](int rt){requestB(rt, [](int rt2){requestC(rt2, [](int rt3){// 根据需要可能会继续嵌套下去});});});
}

这还是好的,如果你使用Qt的信号槽来实现,并同时可能有多个请求,你可能还会遇到另一个问题:“我怎么知道这个返回值是我发送的哪个请求产生的?”如果webapi库没有提供请求与反馈之间互相对应的相关支持,你可能会更加的郁闷。

那么, 使用协程又会有哪些不一样呢?
想象一下, 同样的requestA,requestB,requestC,(当然已经修改为了协程的写法) 你可以这么用

task<void> request()
{int rt = co_await requestA(1);// 处理一些中间结果rt = co_await requestB(rt);// 处理一些中间结果rt = co_await requestC(rt);// 对最终结果做一些事情
}

这三个异步函数会在同一个线程中按照调用顺序依次完成调用。
没错, 不再需要回调函数, 你可以完全顺序的, 仿佛异步调用不存在的使用同步调用的写法。正是因为协程,我们就可以使用一个更加“同步”化的方式,实现异步调用了。
只要一个关键字co_await就能享用。隔壁的JavaScript早就用上了(ES6版本),现在,终于,C++也可以使用了!

那么这么好用的协程,是不是只要C++20一推出,我们加上一个关键字就能直接把异步调用转化为同步调用呢?

很遗憾,并不能。
C++20的协程只是给了我们一个“使用同步风格进行异步调用”的框架,具体的实现还是需要我们自己去做。
如果你对JavaScript中的协程有所了解的话,就会明白,在ES6中,一个函数可以通过await等待返回的前提,是这个函数被声明为async,而这是ES6提供的一个“语法糖”,也就是说,这个关键字只起到“提示”的作用,真正的实现是需要Promise的。
C++20中也是这样,协程是特殊函数,但是在C++20中,这个特殊函数不是由普通函数添加一个关键字组成的,我们需要为实现这个特殊函数做一些额外的工作。
目前,C++20应该不会提供自动化的包装功能,或者简化包装的库,也就是说,想要让某个函数成为协程函数,我们需要人工的做一些额外的工作,一些辅助的自动化的工具应该会在C++23标准中提供,让协程真正的可以被广大开发人员使用。
虽然辅助工具再C++23才会提供,但是最基础的已经在C++20中存在了。

在我们继续讲解之前,先明确一些概念。

co_return,co_yield,co_await是为了使用协程而新增加的三个关键字,这些关键字在非协程函数中是无法使用的。这也就意味着,在main函数中直接调用co_await xxxx(); 是不行的。

这似乎有点违反我们的常识。协程的关键字只能在协程函数中使用有点递归的意思,这难道意味着普通的函数中没法使用协程函数了?这其实是我们一开始听说协程的描述时会产生的一种误解。
为了消除这种误解,我们先了解一下到底什么是协程函数,以及它到底特殊在哪里。

协程函数和Awaitable类

接下来我们先从如何定义协程函数开始:
简单来说,就是如果一个函数的返回值是一个符合Promise规范的类,并且在这个函数中使用了co_return,co_yield,co_await中的一个或多个,那么这个函数就是一个协程函数。
那么Promise规范又是啥?Promise在英文中是许诺的意思。简单来说,Promise规范就是:如果在类A中定义一个叫做promise_type的结构体,并且其中包含特定名字的函数,那么这个类A就符合Promise规范,它就是一个符合Promise规范的类,它也就是一个Promise。
比如以下例子:

struct task{struct promise_type {auto get_return_object() { return task{}; }std::suspend_never initial_suspend() { return {}; }std::suspend_never final_suspend() { return {}; }void return_void() {}void unhandled_exception() {}};
}

由于类task中定义了promise_type,同时其中包含了符合规范的5个函数,它就是一个Promise。
然后根据协程规范,返回这个类的函数就是协程函数,于是如果我们有以下定义:

task getTask() {// 实现中不需要返回task,也不能写returnco_return;
}

getTask()就是一个协程函数了。当然,如果协程函数中不使用co_wait或者co_yield其实就没有什么意义。
然而,我们虽然有了协程函数,但是我们依旧无法使用co_await,为什么呢?因为co_await关键字实际上是一个运算符,其后面只能跟随一个“实现了三个特定函数的类”。这三个特定函数如下所述:2

struct suspend_always {constexpr bool await_ready() const noexcept { return false; }constexpr void await_suspend(std::coroutine_handle<> h) const noexcept {}constexpr void await_resume() const noexcept {}
};

注意,我们实现的时候只需要有包含这三个名字的函数就行了,并不需要继承。

如果我们使用co_await suspend_always(); 会发生什么呢?

  1. suspend_always会被构造,调用其构造函数(一般情况下我们就可以通过构造函数模仿一个普通的函数调用了)。
  2. 通过await_ready()判断是否需要等待,如果返回true,就表示不需要等待,如果返回false,就表示需要等待。
  3. 如果不需要等待,则立刻执行await_resume,否则先执行await_suspend,然后进入等待,调用co_await awaitable(); 的函数会在这里暂停运行,但是不会影响所在线程的执行。
  4. 我们可以在await_suspend函数中通过传统的回调函数法执行一些异步操作,然后在回调函数中调用std::coroutine_handle<>的resume函数主动恢复。
  5. await_resume会在恢复执行后立刻执行,注意:co_wait的返回值就是该函数的返回值,而await_resume函数允许拥有任意的返回值类型,模板类型也是允许的。

也就是说可以使用以下的模板类让co_wait的返回值更加的自由:3

template <class T>
struct someAsyncOpt {bool await_ready()void await_suspend(std::coroutine_handle<>);T await_resume();
};

最后,我们也应该了解,同一个线程在一个时间点最多只能跑一个协程;在同一个线程中,协程的运行是穿行的,没有数据争用(data race),也不需要锁。

至此,我们完成了协程的基本介绍。

那么,到底要如何使用协程呢?

了解了协程后我们就可以发现了以下事实:

  1. 一个线程只能有一个协程
  2. 协程函数需要返回值是Promise
  3. 协程的所有关键字必须在协程函数中使用
  4. 在协程函数中可以按照同步的方式去调用异步函数,只需要将异步函数包装在Awaitable类中,使用co_wait关键字调用即可。

知道了以上事实,我们就可以按照以下方式使用协程了:

  1. 在一个线程中同一个时间只调用一个协程函数,即只有一个协程函数执行完毕了,再去调用另一个协程函数。
  2. 使用Awatiable类包装所有的异步函数,一个异步函数处理一请求中的一部分工作(比如执行一次SQL查询,或者执行一次http请求等)。
  3. 在对应的协程函数中按照需要,通过增加co_wait关键字同步的调用这些异步函数。注意一个异步函数(包装好的Awaiable类)可以在多个协程函数中调用,协程函数可能在多个线程中被调用(虽然一个线程同一时间只调用一个协程函数),所以最好保证Awaiable类是线程安全的,避免出现需要加锁的情况。
  4. 在线程中通过调用不同的协程函数响应不同的请求。

写在最后

协程事实上并没有消灭回调函数,它只是为我们提供了一种方案,让我们可以“用同步调用的方式进行异步调用”。
回调函数还是存在的,只是被实现所隐藏起来了。
同时,协程并不是只能用于“用同步调用的方式进行异步调用”,它的本意其实就是“协同工作”。
也就是我等待你完成某个操作再去执行其它的操作,和多线程类似,但是避免了资源竞争,因为只有一个线程。
所有拥有类似需求的情况都可以使用协程来做。
目前C++20中协程只是刚刚出现,作为一个基础设施存在,因为缺乏必要的辅助支持的库,直接使用协程反而会增加开发的复杂度和困难度。我们可以等待C++23为我们带来一个更好用的协程,而现在我们需要的就是了解而已。


  1. https://lewissbaker.github.io/ ↩︎

  2. C++20标准的草案n4849.pdf http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/n4849.pdf § 17.12.5 ↩︎

  3. C++20标准的草案n4849.pdf http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/n4849.pdf § 7.6.2.3 ↩︎

C++20中的协程(Coroutine)相关推荐

  1. Boost中的协程—Boost.Asio中的coroutine类

    Boost.Asio中有两处涉及协程,本文介绍其中的coroutine类. Boost.Asio中的stackless协程是由coroutine类和一些宏来实现的.coroutine类非常简单,包括四 ...

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

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

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

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

  4. Unity 协程Coroutine综合测试

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

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

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

  6. Python中的协程

    Python中的协程 文章目录 Python中的协程 一.什么是协程 1.概念 2.协程的好处 3.缺点 二.了解协程的过程 1.yield工作原理 2.协程在运行过程中有四个状态: 3.预激协程的装 ...

  7. Python 中 异步协程 的 使用方法介绍

    静觅 崔庆才的个人博客:Python中异步协程的使用方法介绍:https://cuiqingcai.com/6160.html Python 异步 IO .协程.asyncio.async/await ...

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

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

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

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

最新文章

  1. 开源多年后,Facebook这个调试工具,再登Github热门榜
  2. OpenKruise:阿里巴巴 双11 全链路应用的云原生部署基座
  3. oracle子查询为游标结果集,这个SQL怎么破?select后的爬树子查询(connect by)变换
  4. 8086汇编4位bcd码_238期中4头3尾,排列五第19239期爱我彩规
  5. C++中private成员变量和protect成员变量的区别
  6. android 横向铺满,Android开发全程记录(八)——设置ImageView显示的图片铺满全屏(适应魅族等不常见屏幕比例)...
  7. java9 堆外内存_java堆外内存泄漏排查
  8. Python中代码书写规范与基本使用
  9. Android 原生开发、H5、React-Native Flutter 使用利弊和场景技术分享
  10. 洛谷 P3063 [USACO12DEC]牛奶的路由Milk Routing
  11. 根据身份证号 计算具体年龄
  12. Python 实现 2048 游戏
  13. 关于ADC采样的采样频率,采样时间的问题
  14. ArcGIS中的 .tpk数据
  15. 浩辰cad电气2021 安装教程
  16. “杜拉拉思维模式”之五:面试礼仪速成
  17. Q_INVOKABLE与invokeMethod用法详解
  18. E+H超声波液位计FMU41-ARB2A2
  19. 无线网络技术—wimax技术
  20. Elasticsearch(ES6)------(5)kibana的es查询、mysql查询转换和对应javaAPI使用(一)

热门文章

  1. jquery 文件管理
  2. web前端之——图片上传
  3. 计算机系统结构专业考研科目,计算机系统结构考研考哪些科目呢?
  4. ARM汇编 beq和bne %BXX前XXb, %FXX后XXf
  5. [2]无线通信--CDMA多址技术(1)
  6. (英文版)吴恩达机器学习第三周笔记
  7. 学习前端-微信小程序
  8. 专业版网上企业订货平台-移讯云订货系统
  9. 安卓开发报错:“unfortunately,sy6 has stopped”
  10. spring boot 集成druid数据库连接池,并打印sql