浅说 c++20 cppcoro (三)

浅说 c++20 cppcoro (三),https://www.cnblogs.com/bbqzsl/p/18679860

接着上一篇浅说 c++20 coroutine (二) ,继续没说完的事。

先来看co_return 1; 的反编译代码。

再来看co_yield i; 的反编译代码。

比较它们的共通点

这里有一个技巧,co_await suspend_always{} 能够挂起当前协程,控制权返回到caller/resumer。

直到目前,我已经用了两篇作为前置铺垫。

浅说c/c++ coroutine https://www.cnblogs.com/bbqzsl/p/18639898。 介绍stackful协程的一些典型实现,用于清楚分辩stackful跟stackless的实现的差异

浅说 c++20 coroutine (二) https://www.cnblogs.com/bbqzsl/p/18659948。介绍c++20是如何实现stackless,也是cppcoro设计跟实现的基础。

接下来进入cppcoro的篇幅。

先来划分sync, async在cppcoro的意思。sync是我们通常理解的同步阻塞线程。async相对于sync则是不阻塞线程。sync wait则是阻塞线程的同步等待,例如使用多线程同步原语挂起线程等待。async wait则是不阻塞线程的异步等待,所以我们不能够挂起线程,但是我们能够挂起协程。另外异步跟回调是形影不相离般存在的,异步等待就基本是异步回调。再回到co_await。await这个关键词很让人迷惑,await跟wait是同意的,不同的是一个及物动词另一个不及物动词。现在将co_await,展开成 coro asynchronously wait后,就很好理解了。主语coro进行async wait。也就在一个coro里面async wait另一个coro。那么cppcoro实现的一些同步原语,并在名字前加上async,如async_mutex。则是特指用来coro的同步原语,coro的同步原语的"同步"有别于多线程同步原语的同步阻塞线程,coro的"同步"是不阻塞线程的并而是异步的。所以一类以async开头的事物是指用于coro的async wait。

另外,在协程之间,sync与async的关系。当一个常规函数或协程直接使用resume()恢复另一个协程,两者之间不受co_await的协议约定,caller/resumer都会在另一个协程返回控制权(如co_yield),而会继续执行后面的逻辑。它们就是sync的关系,如generator<T>就是sync产生器。如果co_await另一个协程,并且没有挂起就能够完成,返回控制权,这时它们也算是sync的关系,sync call一样。如果co_await另一个协程,协程需要挂起,根据co_await协议的约定,caller/resumer也要挂起。这时它们却是async的关系,async call并由回调恢复它们。async_generator<T>是一个async产生器,因为它支持co_await。而generator<T>是sync产生器,不支持挂起。

还有就是,coro用在所有scheduler的schedule()都算是async。这跟我们的async关键字的意思一样。例如ThreadPool.schedule()在线程池分派,io_servise.schedule()在io_service事件循环中分派。

来看cppcoro。我将cppcoro::task视作一个最小的调度单位,至于task相关的设计思路请移步上一篇。然后我再将resume()调用的地方视为调度。现在来看cppcoro代码里到底有哪些调度。

它们分别有

  1. 基于执行流。task,

  2. 发动器。sync_wait, async_scope, when_all

  3. 基于生产-消费模式。generator, async_generator,

  4. 同步原语。

single_consumer_event

single_consumer_async_auto_reset_event

async_mutex

async_manual_reset_event

async_auto_reset_event

async_latch

sequence_barrier

multi_producer_sequencer

single_producer_sequencer

  1. 线程池调度器。static_thread_pool。

  2. 事件循环调度器。io_service。

先看task<T>。task<T>被实现成挂起链,一旦执行流被某一个co_await的协程挂起,就会沿着co_await回溯出一条挂起链。当那个最里层挂起的协程完成时,就会在final_suspend中恢复它的caller/resumer。重复这个流层直到恢复最外层的协程。它们的角色如下,awaitingCoro co_await (which is async wait) awaiterCoro。awaiterCoro挂起,而awaitingCoro一样要挂起。awaiterCoro完成后,awaiterCoro resume awaitingCoro。

再来看,启动器。我将sync_wait,async_scope,when_all归成一个类别。我们使用它们来启动cppcoro::task<T>。task<T>被设计成lazy任务,initial_suspend()返回suspend_always。我们必须要么用co_await它,要么直接resume()它,才能让它得以继续开始。

当我们的线程还处在非协程上下文时,按照cppcoro的设计意图,我们需要将当前线程切换成协程后,才能启动其它task<T>。因为task<T>将coroutine_handle作为私有成员,我们不得直接去resume()。sync_wait()为我们生成一个sync_wait_task,并由它co_await来启动task<T>。当task<T>完成后,就会调度恢复sync_wait_task。sync_wait_task然后在final_suspend中set_event(),唤醒sync_wait()所在线程。这里有一个技巧,使用co_yield跟yield_value模拟final_suspend(),来避免task<T>的promise析构,保护结果。

当我们的线程已经运行在协程上下文时,就可以使用async_scope对象的spawn()来启动协程。再配合线程池调度器的schedule(),就能够将协程发射到线程池。这里又有一个技术点。在cppcoro的设计中,当一个协程使用调度器进行schedule()后, 关于这个协程的所有执行流都会迁移到调度器的线程(池)。作为一个启动器,我们当然不希望async_scope所在协程,也跟随启动一个协程而被迁移到其它线程。但是co_await的awaiter一定会在完成后,去恢复awaitingCoro。那么就要用一个oneway_task来隔断task<T>的调度关系。

从async_scope的实现来看,协程任务task<T>加了一层oneway_task,oneway_task的final_suspend()不同于task<T>而隔断了async_scope作为awaitingCoro被恢复。同时task<T>也走到终结销毁。可见async_scope毫不关心它启动的协程的结果。join()并不能拿到任何结果。

因此,我们需要when_all()。像async_scope一样,when_all使用when_all_task去co_await其它task<T>。并且when_all()是直接去resume所有when_all_task,而不是用co_await。隔断挂起,隔断恢复链。这样,when_all启动的协程,同样可以用线程池调度器schedule()发射到线程池,不会影响when_all()所在协程。不同于async_scope,when_all()必须在co_await操作中,才会去启动其它任务。通过是设计,async_scope.join()跟when_all()挂起等待一个counter,而不是直接去等待任务。同时when_all,使用了sync_wait一样的技法,用co_yield来保护任务结果。

虽然,由async_scope或when_all来启动的task<T>,并不会因为schedule到线程池,而连随将async_scope或when_all带到线程池,上面讲的隔断挂起的结果。但是async_scope.join()或when_all结束时,它们却都异步等待一个counter。这个counter可以认为是一个async的信号量,是一个async的同步原语。所以它们等待了一个同步原语,并由这个同步原语在某个线程将它们再调度起来。sync_wait()在结束时,阻塞线程等待(Wait)一个线程同步事件。而async_scope.join()或when_all()结束时,则异步等待(Await)一个counter(异步的信号量)。下面是简明图

下面是运行的结果:

再来看同步原语的实现,基本遵从async原则,不阻塞线程,只挂起协程,协程挂起时入链到同步原语的挂起链。当同步原语发起事件时,将挂起链的所有协程恢复。co_await一个同步原语变量, 挂起并异步等待恢复。因为恢复时属于异步回调,所以挂起协程会被调度到同步原语发起事件的线程。

再来看线程池static_thread_pool的调度实现。

再来看io_service是调度实现。

因此io_service可以用作成一个strand串行分派协程任务跟io异步操作。

总的来说,一个cppcoro task<T>在执行过程中挂起时都有一个因缘(,或者挂起源),它可以是另一个task<T>,或者是cppcoro同步原语,线程池,io_service等,并由它们异步调度(恢复回调)。

本篇结束。

相关推荐
萌の鱼2 小时前
leetcode 2466. 统计构造好字符串的方案数
数据结构·c++·算法·leetcode
TwilightLemon3 小时前
C++ 使用MIDI库演奏《晴天》
c++
ChoSeitaku5 小时前
NO.13十六届蓝桥杯备战|条件操作符|三目操作符|逻辑操作符|!|&&|||(C++)
c++·职场和发展·蓝桥杯
ByteDreamer5 小时前
C/C++内存管理
开发语言·c++
ox00806 小时前
C++ 设计模式-桥接模式
c++·设计模式·桥接模式
上元星如雨7 小时前
详解C++的存储区
java·开发语言·c++
ox00807 小时前
C++ 设计模式-原型模式
c++·设计模式·原型模式
tamak8 小时前
c/c++蓝桥杯经典编程题100道(21)背包问题
c语言·c++·蓝桥杯
c-c-developer9 小时前
C++ Primer 条件语句
c++