c++20的协程该怎么使用?
【CSDN 萨德基】一年前,C++20 正式宣布正式发布。在这一版,开发人员总算迎了PulseAudio优点,它可以让标识符非常清新,单纯简练,同时保持了触发器的高效能。但不少开发人员坦言,C++的PulseAudio国际标准是给库的开发人员采用的,比较复杂,对一般开发人员一点都不亲善。在这首诗中,C++ 现职技术专家祁宇着眼于 C++20 采用的无栈PulseAudio国际标准,以具体内容实例撷取PulseAudio的具体内容应用课堂教学与经验。 译者 | 祁宇,许巨作,韩垚 白眉林 | 屠敏 公司出品 | CSDN(ID:CSDNnews) 经过多年的筹划、争辩、准备后,PulseAudio总算进入 C++20 国际标准。 PulseAudio并不是一个捷伊概念,它旧石器时代已经有数十年的历史了,也已然存在于很多其他C语言(Python、C、Go)。 PulseAudio分为无栈PulseAudio和有栈PulseAudio两种,无栈指可挂起/恢复的表达式,有栈PulseAudio则相等于采用者态缓存。有栈PulseAudio转换的生产成本是采用者态缓存转换的生产成本,而无栈PulseAudio转换的生产成本则相等于解释器的生产成本。 无栈PulseAudio和缓存的差别:无栈PulseAudio只能被缓存初始化,本身并不抢占市场Mach运维,而缓存则可抢占市场Mach运维。 C++20 PulseAudio中接纳的是谷歌明确提出并主导力量(来源于 C)的无栈PulseAudio。很多人反对这个优点,主要槽点包括:极难认知、过分灵巧、Contiki导致的操控性问题等等。Google 对该决议案发动了一系列聊著并试著得出了有栈PulseAudio的方案。有栈PulseAudio比系统级缓存高效能很多,但较之无栈PulseAudio还是差了很多。 由于 C++ 的设计神学是"Zero Overhead Abstractions",最终无栈PulseAudio正式宣布成为了 C++20 PulseAudio国际标准。 当今 C++ 世界进化的三大前奏是触发器化与博戈达化。而 C++20 PulseAudio能够以并行句法写触发器标识符的优点,使其正式宣布成为撰写触发器标识符的好辅助工具,触发器库的PulseAudio化将是必然趋势,因此很有必要性掌控 C++20 PulseAudio。 通过一个单纯的范例来展现一下PulseAudio的诀窍。 基于回调的触发器client的伪标识符 基于触发器回调的 client 流程如下: 这个标识符有很多回调表达式,采用回调的时候还有一些陷阱,比如如何保证安全的回调、如何让触发器读实现触发器递归初始化,如果再结合触发器业务逻辑,回调的嵌套层次会更深,我们已经看到 callback hell 的影子了!可能也有读者觉得这个程度的触发器回调还可以接受,但是如果工程变大,业务逻辑变得更加复杂,回调层次越来越深,维护起来就很困难了。 再来看看用PulseAudio是怎么写这个标识符的: 基于C++20PulseAudio的触发器client 同样是触发器 client,相比回调模式的触发器 client,整个标识符非常清新,单纯简练,同时保持了触发器的高效能,这就是 C++20 PulseAudio的威力! 相信你看了这个范例之后应该不会再想用触发器回调去写标识符了吧,是时候拥抱PulseAudio了! 有栈(stackful)PulseAudio通常的实现手段是在堆上提前分配一块较大的内存空间(比如 64K),也就是PulseAudio所谓的栈,参数、return address 等都可以存放在这个栈空间上。如果需要PulseAudio转换,那么通过 swapcontext 一类的形式来让系统认为这个堆上空间就是一般的栈,这就实现了上下文的转换。 有栈PulseAudio最大的优势就是侵入性小,采用起来非常简便,已有的业务标识符几乎不需要做什么修改,但是 C++20 最终还是选择了采用无栈PulseAudio,主要出于下面这几个方面的考虑。 有栈PulseAudio的栈空间普遍是比较小的,在采用中有栈溢出的风险;而如果让栈空间变得很大,对内存空间又是很大的浪费。无栈PulseAudio则没有这些限制,既没有溢出的风险,也无需担心内存利用率的问题。 有栈PulseAudio在转换时确实比系统缓存要高效能,但是和无栈PulseAudio相比仍然是偏重的,这一点虽然在我们目前的实际采用中影响没有那么大(触发器系统的采用通常伴随了 IO,相比于转换开销多了几个数量级),但也决定了无栈PulseAudio可以用在一些更有意思的场景上。举个范例,C++20 coroutines 决议案的译者 Gor Nishanov 在 CppCon 2018 上演示了无栈PulseAudio能做到纳秒级的转换,并基于这个特点实现了减少 Cache Miss 的优点。 无栈PulseAudio是一个可以暂停和恢复的表达式,是解释器的泛化。 为什么? 我们知道一个表达式的表达式体(function body)是顺序执行的,执行完之后将结果返回给初始化者,我们没办法挂起它并稍后恢复它,只能等待它结束。而无栈PulseAudio则允许我们把表达式挂起,然后在任意需要的时刻去恢复并执行表达式体,相比一般表达式,PulseAudio的表达式体可以挂起并在任意时刻恢复执行。 所以,从这个角度来说,无栈PulseAudio是一般表达式的泛化。 C++20 提供了三个新关键字(co_await、co_yield 和 co_return),如果一个表达式中存在这三个关键字之一,那么它就是一个PulseAudio。 编译器会为PulseAudio生成很多标识符以实现PulseAudio语义。会生成什么样的标识符?我们怎么实现PulseAudio的语义?PulseAudio的创建是怎样的?co_await机制是怎样的?在探索这些问题之前,先来看看和 C++20 PulseAudio相关的一些基本概念。 当 caller 初始化一个PulseAudio的时候会先创建一个PulseAudio帧,PulseAudio帧会构建 promise 对象,再通过 promise 对象产生 return object。 PulseAudio帧中主要有这些内容: 这些内容在PulseAudio恢复运行的时候需要用到,caller 通过PulseAudio帧的句柄 std::coroutine_handle 来访问PulseAudio帧。 promise_type 是 promise 对象的类型。promise_type 用于定义一类PulseAudio的行为,包括PulseAudio创建方式、PulseAudio初始化完成和结束时的行为、发生异常时的行为、如何生成 awaiter 的行为以及 co_return 的行为等等。promise 对象可以用于记录/存储一个PulseAudio实例的状态。每个PulseAudio桢与每个 promise 对象以及每个PulseAudio实例是一一对应的。 它是promise.get_return_object()方法创建的,一种常见的实现手法会将 coroutine_handle 存储到 coroutine object 内,使得该 return object 获得访问PulseAudio的能力。 PulseAudio帧的句柄,主要用于访问底层的PulseAudio帧、恢复PulseAudio和释放PulseAudio帧。 程序员可通过初始化 std::coroutine_handle::resume() 唤醒PulseAudio。 co_await expr 通常用于表示等待一个任务(可能是 lazy 的,也可能不是)完成。co_await expr 时,expr 的类型需要是一个 awaitable,而该 co_await表达式的具体内容语义取决于根据该 awaitable 生成的awaiter。 看起来和PulseAudio相关的对象还不少,这正是PulseAudio复杂又灵巧的地方,可以借助这些对象来实现对PulseAudio的完全控制,实现任何想法。但是,需要先要了解这些对象是如何协作的,把这个搞清楚了,PulseAudio的原理就掌控了,写PulseAudio应用也会游刃有余了。 以一个单纯的标识符展现这些PulseAudio对象如何协作: Return_t:promise return object。 awaiter: 等待一个task完成。 PulseAudio运行流程图 图中浅蓝色部分的方法就是 Return_t 关联的 promise 对象的表达式,浅红色部分就是 co_await 等待的 awaiter。 这个流程的驱动是由编译器根据PulseAudio表达式生成的标识符驱动的,分成三部分: PulseAudio的创建 foo()PulseAudio会生成下面这样的模板标识符(伪标识符),PulseAudio的创建都会产生类似的标识符: 首先需要创建PulseAudio,创建PulseAudio之后是否挂起则由初始化者设置 initial_suspend 的返回类型来确定。 创建PulseAudio的流程大概如下: 在这个模板框架里有一些可定制点:如 initial_suspend、final_suspend、unhandled_exception 和 return_value。 我们可以通过 promise 的 initial_suspend 和 final_suspend 返回类型来控制PulseAudio是否挂起,在 unhandled_exception 里处理异常,在 return_value 里保存PulseAudio返回值。 可以根据需要定制 initial_suspend 和 final_suspend 的返回对象来决定是否需要挂起PulseAudio。如果挂起PulseAudio,标识符的控制权就会返回到caller,否则继续执行PulseAudio表达式体(function body)。 另外值得注意的是,如果禁用异常,那么生成的标识符里就不会有 try-catch。此时PulseAudio的运行效率几乎等同非PulseAudio版的一般表达式。这在嵌入式场景很重要,也是PulseAudio的设计目的之一。 co_await 操作符是 C++20 新增的一个关键字,co_await expr 一般表示等待一个惰性求值的任务,这个任务可能在某个缓存执行,也可能在 OS Mach执行,什么时候执行结束不知道,为了操控性,我们又不希望阻塞等待这个任务完成,所以就借助 co_await 把PulseAudio挂起并返回到 caller,caller 可以继续做事情,当任务完成之后PulseAudio恢复并拿到 co_await 返回的结果。 所以 co_await 一般有这几个作用: 编译器会根据 co_await expr 生成这样的标识符: ;
using await_suspend_result_t =
decltype(awaiter.await_suspend(handle_t::from_promise(p)));
//挂起PulseAudio
if constexpr (std::is_void_v)
awaiter.await_suspend(handle_t::from_promise(p)); //触发器(也可能并行)执行task
//返回给caller
std::is_same_v,
"await_suspend() must return void or bool.");
if (awaiter.await_suspend(handle_t::from_promise(p)))
//task执行完成,恢复PulseAudio,这里是PulseAudio恢复执行的地方
return awaiter.await_resume(); //返回task结果 这个标识符执行流程就是PulseAudio运行流程图中粉红色部分,从这个生成的标识符可以看到,通过定制 awaiter.await_ready() 的返回值就可以控制是否挂起PulseAudio还是继续执行,返回 false 就会挂起PulseAudio,并执行 awaiter.await_suspend,通过 awaiter.await_suspend 的返回值来决定是返回 caller 还是继续执行。 正是 co_await 的这种机制是变触发器回调为并行的关键。 C++20 PulseAudio中最重要的两个对象就是 promise 对象(恢复PulseAudio和获取某个任务的执行结果)和 awaiter(挂起PulseAudio,等待task执行完成),其他的都是辅助工具人,要实现想要的的PulseAudio,关键是要设计如何让这两个对象协作好。 关于co_await的更多细节,读者可以看这个文档(https://lewissbaker.github.io/2017/11/17/understanding-operator-co-await)。 再回过头来看这个单纯的PulseAudio: foo PulseAudio只有三行标识符,但它最终生成的是一百多行的标识符, 如论是PulseAudio的创建还是 co_await 机制都是由这些标识符实现的,这就是 C++20 PulseAudio的微言大义。 关于 C++20 PulseAudio的概念和实现原理已经讲了很多了,接下来通过一个单纯的 C++20 PulseAudio实例来展现PulseAudio是如何运行的。 这个范例很单纯,通过 co_await 把PulseAudio运维到一个缓存中打印一下缓存 id。 测试输出: 从这个输出可以清晰的看到PulseAudio是如何创建的、co_await 等待缓存结束、缓存结束后PulseAudio返回值以及PulseAudio销毁的整个过程。 输出内容中的 1、2、3 展现了PulseAudio创建过程,先创建 promise,再通过 promise.get_return_object() 返回 task,这时PulseAudio就创建完成了。 PulseAudio创建完成之后是要立即执行PulseAudio表达式呢?还是先挂起来?这个行为由 promise.initial_suspend() 来确定,由于它返回的是一个 std::suspend_never的awaiter,所以不会挂起PulseAudio,于是就立即执行PulseAudio表达式了。 执行PulseAudio到表达式的 co_await awaiter 时,是否需要等待某个任务?返回 false 表明希望等待,于是接着进入到 awaiter.wait_suspend(),并挂起PulseAudio,在 await_suspend 中创建了一个缓存去执行任务(注意PulseAudio具柄传入到缓存中了,以便后面在缓存中恢复PulseAudio),之后就返回到 caller了,caller 这时候可以不用阻塞等待缓存结束,可以做其他事情。注意:这里的 awaiter 同时也是一个 awaitable,因为它支持 co_await。 更多时候我们在缓存完成之后才去恢复PulseAudio,这样可以告诉挂起等待任务完成的PulseAudio:任务已经完成了,现在可以恢复了,PulseAudio恢复后拿到任务的结果继续执行。 当缓存开始运行的时候恢复挂起的PulseAudio,这时候标识符执行会回到PulseAudio表达式继续执行,这就是最终的目标:在一个新缓存中去执行PulseAudio表达式的打印语句。 awaiter.final_suspend 决定是否要自动销毁PulseAudio,返回 std::suspend_never 就自动销毁PulseAudio,否则需要采用者手动去销毁。 再回过头来看PulseAudio表达式: 输出结果显示 co_await 上面和下面的缓存是不同的,以 co_await 为分界线,co_await 之上的标识符在一个缓存中执行,co_await 之下的标识符在另外一个缓存中执行,一个PulseAudio表达式跨了两个缓存,这就是PulseAudio的魔法。本质是因为在另外一个缓存中恢复了PulseAudio,恢复后标识符的执行就在另外一个缓存中了。 另外,这里没有展现如何等待一个PulseAudio完成,单纯的采用了缓存休眠来实现等待的,如果要实现等待PulseAudio结束的逻辑,标识符还会增加一倍。 相信你通过这个单纯的范例对 C++20 PulseAudio的运行机制有了更深入的认知,同时也会感叹,PulseAudio的采用真的只适合库译者,一般的开发人员想用 C++20 PulseAudio还是挺难的,这时就需要PulseAudio库了,PulseAudio库可以大幅降低采用PulseAudio的难度。 通过前面的介绍可以看到,C++20 PulseAudio还是比较复杂的,它的概念多、细节多,又是编译器生成的模板框架,又是一些可定制点,需要了解如何和编译器生成的模板框架协作,这些对于一般的采用者来说光认知就比较吃力,更逞论灵巧运用了。 这时也可以认知为什么当初 Google 聊著这样的PulseAudio决议案极难认知、过分灵巧了,然而它的确可以让我们仅需要通过定制化一些特定方法就可以随心所欲的控制PulseAudio,还是很灵巧的。 总之,这就是 C++20 PulseAudio,它目前只适合给库译者采用,因为它只提供了一些底层的PulseAudio原语和一些PulseAudio暂停和恢复的机制,一般采用者如果希望采用PulseAudio只能依赖PulseAudio库,由PulseAudio库来屏蔽这些底层细节,提供单纯易用的 API。因此,我们迫切需要一个基于 C++20 PulseAudio封装好的单纯易用的PulseAudio库。 正是在这种背景下,C++20 PulseAudio库 async_simple(https://github.com/alibaba/async_simple)就应运而生了! 阿里巴巴开发的 C++20 PulseAudio库,目前广泛应用于图计算引擎、时序数据库、搜索引擎等在线系统。连续一年经历天猫双十一磨砺,承担了亿级别流量洪峰,具备非常强劲的操控性和可靠的稳定性。 async_simple 现在已经在 GitHub 上开源,有了它你在也不用为 C++20 PulseAudio的复杂而苦恼了,正如它的名字一样,让触发器变得单纯。 接下来我们将介绍如何采用 async_simple 来简化触发器编程。 async_simple 提供了丰富的PulseAudio组件和单纯易用的 API,主要有: 关于 async_simple 的更多介绍和实例,可以看 GitHub(https://github.com/alibaba/async_simple/tree/main/docs/docs.cn)上的文档。 有了这些常用的丰富的PulseAudio组件,我们写触发器程序就变得很单纯了,通过之前打印缓存 id 范例来展现如何采用 async_simple 来实现它,也可以对比下用PulseAudio库的话,标识符会单纯多少。 借助 async_simple 可以轻松地把PulseAudio运维到 executor 缓存中执行,整个标识符变得非常清新,单纯简练,标识符量相比之前少得多,采用者也不用去关心 C++20 PulseAudio的诸多细节了。 借助 async_simple 这个PulseAudio库,可以轻松的让 C++20 PulseAudio这只王谢堂前燕,飞入寻常百姓家! async_simple 提供了很多 example,比如采用 async_simple 开发 http client、http server、smtp client 等实例,更多 Demo 可以看 async_simple 的 demo example(https://github.com/alibaba/async_simple/blob/main/demo_example)。 采用 async_simple 中的 Lazy 与 folly 中的 Task 以及cppcoro中的 task 进行比较,对无栈PulseAudio的创建速度与转换速度进行操控性测试。需要说明的是,这只是一个高度裁剪的测试用于单纯展现 async_simple,并不做任何操控性比较的目的。而且 Folly::Task 有着更多的功能,例如 Folly::Task 在转换时会在 AsyncStack 记录上下文以增强程序的 Debug 便利性。 CPU: Intel® Xeon® Platinum 8163 CPU @ 2.50GHz 单位: 纳秒,数值越低越好。 测试结果表明 async_simple 的操控性还是比较出色的,未来还会持续去优化改进。 C++20 PulseAudio像一台精巧的机器,虽然复杂,但非常灵巧,允许我们去定制化它的一些零件,通过这些定制化的零件我们可以随心所欲的控制这台机器,让它帮我们实现任何想法。 正是这种复杂性和灵巧性让 C++20 PulseAudio的采用变得困难,幸运的是我们可以采用工业级的成熟易用的PulseAudio库 async_simple 来简化PulseAudio的采用,让触发器变得单纯! 参考资料:1.谷歌明确提出并主导力量的无栈PulseAudio正式宣布成为C++20PulseAudio国际标准
2.C++20 为什么选择无栈PulseAudio?
无栈PulseAudio是一般表达式的泛化
3.C++20 PulseAudio的微言大义
PulseAudio相关的对象
PulseAudio帧(coroutine frame)
promise_type
coroutine return object
std::coroutine_handle
co_await、awaiter、awaitable
co_await:一元操作符;awaitable:支持 co_await 操作符的类型;awaiter:定义了 await_ready、await_suspend 和 await_resume 方法的类型。PulseAudio对象如何协作
co_await 机制
微言大义
4.一个单纯的 C++20 PulseAudio范例
PulseAudio创建
PulseAudio创建后的行为
co_await awaiter
PulseAudio恢复
PulseAudio销毁
PulseAudio的魔法
5.为什么需要一个PulseAudio库
6.async_simple 让PulseAudio变得单纯
7.操控性
测试硬件
测试结果
8.总结
相关文章
发表评论
评论列表
- 这篇文章还没有收到评论,赶紧来抢沙发吧~