写点什么

Code Like Sync, Works Like Async

发布于: 2020 年 09 月 17 日
Code Like Sync, Works Like Async

小编推荐: 异步编程是开发者绕不开的问题,在嵌入式场景更具其独特性,本文希望能在“用户态Scheduler”与“标签语法糖”两个角度给出一些思考,期待收获读者的探讨和共鸣。



前言.

打从敲下"Hello World!",咱们算入行了。我开始写代码,和用计算器差不多:

  • 输入:1 + 1

  • 结果:2

  • ...

代码一路顺着走,不开叉。 



生涯第1个波折,是学到条件判断,像这样:

  • a = 1,b =  1

  • if a + b = 2    print "这都知道?"else    print "答对了"

代码开始有不同走向,有点绕,还能驾驭。

 

再后来就遇到人生大哉问:妈和媳妇一起掉河里,你救谁?

翻译一下,如何同时运行代码A和代码B(并行)?

为一起救,我学过各种泳姿:

  • 开始用 “定时器/软中断”挺好,不过随着媳妇数(代码块)的增多,猝!

  • 后来折腾“事件驱动/状态机”,就活在连跳/回调地狱里,最终为个delay而累猝!

  • 最后“多线程”还行......还活着。

焦头烂额,终于hold住。

 

可人生问题哪止这些,后来更大问题是3字:“没钱”

“多线程”虽好,太费钱(资源消耗大)、太折腾(竞争成本高),很多场景受限:

  • 浏览器:没有多线程

  • 嵌入式MCU:实时性要求高、RAM受限,甚至没有RTOS

  • 大链接数server:微信后台

  • 游戏:魔兽世界

吐血。

小品文已过,咱们言归正传。



1.

近些年,很多编程语言都修订标准,新增2个关键字async/await:

  • 直接包含:C#/JavaScript/Rust/Dart

  • 其它语言(golang/erlang/lua...)没直接包含,也提供类似机制 ,比如Go goroutine

所有目的,都是为更好的支持异步编程,不约而同的加强Coroutine。

 

Coroutine(协程)是老东西(比UNIX都早),近些年的广泛使用有其背景:

  • 大量线程用不起(服务器或嵌入式),或者没有线程(浏览器或MCU裸机)

  • 然而,异步需求在,开源库API设计成异步IO渐变主流

  • 可异步代码太难搞,程序一膨胀,不是回调地狱就是全程乱跳。。。

  • 由此不约而同想:代码能不能写起来像同步,跑起来像异步?

  • 做梦之余一看,这不是就是Coroutine吗?好好,不止第三方库,要语言层支持,加promise,再加async/await

嗯,这段像瞎掰,实际是典型JavaScript开发者心态,其它也类似。

 

远的不说,做嵌入式开发,也常被异步IO编程(基于中断/消息/事件)搞得焦头烂额,我也想要“Code like sync, works like async”。

可C/C++如何做Coroutine呢?



2.

方案1:

基于用户态Scheduler(微信后台方案)

Coroutine是简单的东西,第一反应就是拿用户态Scheduler实现。

即任务切换时,完成真实的换栈:





  • 所有Coroutines共享1个CPU线程,相互无抢占(协作式,竞争成本低)

  • Coroutine更轻,尽管Linux中单线程只需约4KB(1页),然而1个Coroutine只0.1KB

  • Coroutine切换快(约100T),在多数平台上消耗时间在纳秒级/微秒级

对大量链接的服务器后台,上述优势明显。可以参见腾讯开源协程库: libco。

 

这是对调用者干净的实现,编程没有限制,PC端主流协程库都如此实现:

libco(微信)/Boost.Context(C++准标准)/libaco/coroutine



换栈的手段也不局限汇编,有系统库或第三方:

  • posix中context/Windows中fiber/C标准中setjump等。

尽管如此,可对嵌入式编程意义很小:

RTOS中Task实现类似,内存消耗/切换成本已经很低

RTOS中Task亦可配置成协作式(非抢占)

每种CPU都需要移植,等价于再实现Scheduler,意义不大



方案2:

基于标签语法糖(switch或goto扩展)

这是借用预编译宏和标签,大玩语法游戏的套路。

目前所有类似实现(如Protothread),都基于Simon Tatham的文章,这里简要1种核心:





把上述switch/case手段用宏crBegin/crFinish/crReturn包装下,下面两段带while(1)的死循环代码,就可以在1个线程中并发执行:





这是Coroutine实现的最轻方式,优点:

  • 完全标准C实现,跨平台

  • 每个Coroutine只需要额外1字节 - 2字节内存

  • Task切换无成本

不过,不知道大家发没发现,局部变量该怎么办?

没有换栈,无法用局部变量,必须static静态化。由此,这种方式受限明显:

  • 如果用static变量,会导致函数无法重入

  • 如果不用static,就必须传参类似ctx参数来代替局部变量

看的出来,让调用者有些难受。

 

由此,Contiki OS又对上述方法改良并包装,并用这种机制实现整个操作系统的多任务。

它的代码值得一读,除了这部分,其它模块(uIP/GUI/Timer/Mem)都很漂亮。



3.

大家看的出来,嵌入式系统上1和2还不够完美,有没有更好的方案?希望:只用C99实现,完全跨平台/跨架构内存消耗要远小于线程,额外消耗在几个字节内可以使用局部变量(至少表面上)对调用者友好,类似其它语言async/await的方式显然需要新方案。这里卖个关子,感兴趣的朋友可以留言讨论,请持续关注我们公众号哦~





发布于: 2020 年 09 月 17 日阅读数: 212
用户头像

公众号:普惠出行产品技术 2020.07.31 加入

这里是滴滴出行旗下普惠产品技术团队对外分享的窗口,普惠出行支撑滴滴代驾、货运等业务,建设了NodeX、Dokit、卡梅隆等开源项目,始终秉承聚心成事、聚气育人的原则。欢迎各位技术同仁一起交流,共同成长~

评论

发布
暂无评论
Code Like Sync, Works Like Async