C++20中的协程(Coroutine)的实现

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,也不能写return
 co_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(); 会发生什么呢?

  • suspend_always会被构造,调用其构造函数(一般情况下我们就可以通过构造函数模仿一个普通的函数调用了)。
  • 通过await_ready()判断是否需要等待,如果返回true,就表示不需要等待,如果返回false,就表示需要等待。
  • 如果不需要等待,则立刻执行await_resume,否则先执行await_suspend,然后进入等待,调用co_await awaitable(); 的函数会在这里暂停运行,但是不会影响所在线程的执行。
  • 我们可以在await_suspend函数中通过传统的回调函数法执行一些异步操作,然后在回调函数中调用std::coroutine_handle<>的resume函数主动恢复。
  • 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),也不需要锁。

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

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

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

一个线程只能有一个协程

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

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

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

写在最后

协程事实上并没有消灭回调函数,它只是为我们提供了一种方案,让我们可以“用同步调用的方式进行异步调用”。

回调函数还是存在的,只是被实现所隐藏起来了。

同时,协程并不是只能用于“用同步调用的方式进行异步调用”,它的本意其实就是“协同工作”。
也就是我等待你完成某个操作再去执行其它的操作,和多线程类似,但是避免了资源竞争,因为只有一个线程。
所有拥有类似需求的情况都可以使用协程来做。

目前C++20中协程只是刚刚出现,作为一个基础设施存在,因为缺乏必要的辅助支持的库,直接使用协程反而会增加开发的复杂度和困难度。我们可以等待C++23为我们带来一个更好用的协程,而现在我们需要的就是了解而已。

参考链接

https://lewissbaker.github.io/

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

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

到此这篇关于C++20中的协程(Coroutine)的实现的文章就介绍到这了,更多相关C++20 协程 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 在python里协程使用同步锁Lock的实例

    尽管asyncio库是使用单线程来实现协程的,但是它还是并发的,乱序执行的.可以说是单线程的调度系统,并且由于执行时有延时或者I/O中断等因素,每个协程如果同步时,还是得使用一些同步对象来实现. 比如asyncio就定义了一个锁对象Lock,它一次只允许一个协程来访问共享的资源,如果多协程想访问就会阻塞起来,也就是说如果一个协程没有释放这个锁,别的协程是没有办法访问共享的资源. 例子: import asyncio import functools def unlock(lock): print

  • python asyncio 协程库的使用

    asyncio 是 python 力推多年的携程库,与其 线程库 相得益彰,更轻量,并且协程可以访问同一进程中的变量,不需要进程间通信来传递数据,所以使用起来非常顺手. asyncio 官方文档写的非常简练和有效,半小时内可以学习和测试完,下面为我的一段 HelloWrold,感觉可以更快速的帮你认识 协程 . 定义协程 import asyncio import time async def say_after(delay, what): await asyncio.sleep(delay)

  • Django如何使用asyncio协程和ThreadPoolExecutor多线程

    Django视图函数执行,不在主线程中,直接loop = asyncio.new_event_loop() # 不能loop = asyncio.get_event_loop() 会触发RuntimeError: There is no current event loop in thread 因为asyncio程序中的每个线程都有自己的事件循环,但它只会在主线程中为你自动创建一个事件循环.所以如果你asyncio.get_event_loop在主线程中调用一次,它将自动创建一个循环对象并将其设

  • 详细解读tornado协程(coroutine)原理

    tornado中的协程是如何工作的 协程定义 Coroutines are computer program components that generalize subroutines for nonpreemptive multitasking, by allowing multiple entry points for suspending and resuming execution at certain locations.. -- [ 维基百科 ] 我们在平常编程中,更习惯使用的是子

  • python中协程实现TCP连接的实例分析

    在网络通信中,每个连接都必须创建新线程(或进程) 来处理,否则,单线程在处理连接的过程中, 无法接受其他客户端的连接.所以我们尝试使用协程来实现服务器对多个客户端的响应. 与单一TCP通信的构架一样,只是使用协程来实现多个任务同时进行. #服务端 import socket from gevent import monkey import gevent monkey.patch_all() def handle_conn(seObj): while True: re_Data = seObj.r

  • Kotlin学习教程之协程Coroutine

    定义 Coroutine翻译为协程,Google翻译为协同程序,一般也称为轻量级线程,但需要注意的是线程是操作系统里的定义概念,而协程是程序语言实现的一套异步处理的方法. 在Kotlin文档中,Coroutine定义为一个可被挂起的计算实例,下面话不多说了,来一起看看详细的介绍吧. 配置 build.gradle中dependencies 添加下面2行,注意coroutine目前仍处于experiment阶段,但Kotline官方保证向前兼容. dependencies { implementa

  • Python使用monkey.patch_all()解决协程阻塞问题

    直接参考以下实例,采用协程访问三个网站 由于IO操作非常耗时,程序经常会处于等待状态 比如请求多个网页有时候需要等待,gevent可以自动切换协程 遇到阻塞自动切换协程,程序启动时执行monkey.patch_all()解决 # 由于IO操作非常耗时,程序经常会处于等待状态 # 比如请求多个网页有时候需要等待,gevent可以自动切换协程 # 遇到阻塞自动切换协程,程序启动时执行monkey.patch_all()解决 # 首行添加下面的语句即可 from gevent import monke

  • Python并发编程协程(Coroutine)之Gevent详解

    Gevent官网文档地址:http://www.gevent.org/contents.html 基本概念 我们通常所说的协程Coroutine其实是corporateroutine的缩写,直接翻译为协同的例程,一般我们都简称为协程. 在linux系统中,线程就是轻量级的进程,而我们通常也把协程称为轻量级的线程即微线程. 进程和协程 下面对比一下进程和协程的相同点和不同点: 相同点: 我们都可以把他们看做是一种执行流,执行流可以挂起,并且后面可以在你挂起的地方恢复执行,这实际上都可以看做是con

  • python 使用事件对象asyncio.Event来同步协程的操作

    事件对象asyncio.Event是基于threading.Event来实现的. 事件可以一个信号触发多个协程同步工作, 例子如下: import asyncio import functools def set_event(event): print('setting event in callback') event.set() async def coro1(event): print('coro1 waiting for event') await event.wait() print(

  • 基于asyncio 异步协程框架实现收集B站直播弹幕

    前言 虽然标题是全站,但目前只做了等级 top 100 直播间的全天弹幕收集. 弹幕收集系统基于之前的B 站直播弹幕姬 Python 版修改而来.具体协议分析可以看上一篇文章. 直播弹幕协议是直接基于 TCP 协议,所以如果 B 站对类似我这种行为做反制措施,比较困难.应该有我不知道的技术手段来检测类似我这种恶意行为. 我试过同时连接 100 个房间,和连接单个房间 100 次的实验,都没有问题.>150 会被关闭链接. 直播间的选取 现在弹幕收集系统在选取直播间上比较简单,直接选取了等级 to

随机推荐