vlambda博客
学习文章列表

为什么编程语言对异步编程都是很晚近才开始支持的?

很多语言前期都没有异步编程的关键字,像async和await之类的。可是异步的思想应该很早之前就产生了,为什么编程语言对其的支持却出现的很晚呢?


因为“直接在语言层面支持异步”并不是必需的;尤其是在经过大量实践、真正把各种异步模型的优缺点彻底摸清之前,“语言层面的异步支持”反而是笨拙的、多余的。

异步模型本来就有很多很多种。从早期的中断服务模型、多进程协同模型再到轻量级进程、线程乃至协程,业界也走过了很多弯路。

比如,早期的Windows 3.X搞的协作时多任务就是协程思路管理的进程,在开发者良莠不齐甚至抱有敌意的环境下这么搞完全是自寻烦恼;于是到了Windows 95就改用了“抢夺式多任务”调度方案。

再比如,线程刚刚兴起时,Linus坚持认为Linux的进程已经足够用了,而且还有轻量级进程可以用;这致使Linux有很多年都不能支持线程(可以通过库来支持,但库做不到“一个进程内的多个线程同时利用多个CPU核心”:其实不怕麻烦的话,用进程来实现线程、用共享内存模拟“可共享的进程内资源”,也还是可以写出“有多CPU支持的‘线’程库”的,但那就有点行为艺术了)。


那时只有服务器才较多使用了多CPU主板——注意和现在的多核心不同,多CPU主板上面有两个以上的CPU插槽,允许插多颗物理CPU;而且多颗物理CPU各自有自己的内存,不同CPU之间要访问对方的内存就必须通过进程间通讯机制挤总线传输数据。

那么,共享进程资源的线程显然并不能从中得到任何好处——因此,Linus的决定显然极有道理。


但是,CPU频率提升遇到了瓶颈,多核多线程时代来临。

“多线程CPU”指的是单个CPU核心内部制作了超量的逻辑单元,比如多个ALU、多个取指/译码电路,等等;这种CPU除非跑MMX/SSE/AVX之类指令,否则内部的大部分逻辑单元是不可能被充分利用的;但如果你有两条线程,它们就可以较为充分的利用这一颗CPU核心内部的大量逻辑单元了。

“多核CPU”则是把多个CPU核心封装在同一块芯片内部,物理上看是一颗CPU,插在只有一个CPU插槽的主板上、使用同一组内存条;但实际上,这颗CPU内部有若干个物理的CPU核心,你完全可以给它们分别安排不同的任务……

不仅如此。

当年AMD最早把两颗CPU核心做到同一片硅晶片上,首先推出了双核CPU;Intel仓皇应对,把两颗奔腾D晶片封装进去,也推出了自己的双核产品——后者被网友调侃为“胶水双核”。双方很是打了一番口水战。


实际上,当年的竞争还要激烈得多,复杂得多。

比如,RISC和CISC之争:CISC太复杂了,为了兼容,甚至连8086的指令都能在Pentium上跑,这得浪费多少芯片面积、给编译器造成多少麻烦……RISC决定抛掉兼容性包袱,精心优化指令集,限制缓慢的内存访问指令的数量、把节省下来的芯片面积多造寄存器、通过寄存器窗口切换寄存器组,使得函数调用/线程切换不影响执行效率;同时由于它的指令集更简洁,编译器优化就更容易……

又有人说,我们干嘛不在一颗CPU里面造很多很多逻辑单元呢?然后在编译器上下功夫,把大量用户操作整合进一条指令——就好像“背包问题”一样,一条指令整合尽可能多的操作、尽可能的充分利用CPU资源……这不就可以最大限度的提升指令执行效率了吗?

这就是所谓的“超长指令字计算机”。

此外,还有超级标量CPU等很多奇思妙想。这些东西都失败了;但这并不等于说它们都是错的;相反,它们仅仅是“商业上未能取得成功”而已;它们的思路还是被现代CPU汲取、整合进来了。比如,指令多发射、SIMD以及RISC的很多先进经验,现在都整合在x86里面了。

显然,CPU架构该如何发展,就连Intel/AMD都说不准。人类的能力,只能做到“走一步看一看”“提出一大堆看起来很美好的理论,到市场上比比优劣”,然后淘汰掉不合时宜的、保留其中更为优秀的。

甚至于,很多东西的走向仅仅决定于一个偶然——比如,如果不是AMD逼迫,或许intel就不会搞“胶水双核”方案;那么现在多核CPU的核心之间的联系可能就会紧密得多,甚至直接走“超长指令字计算机”的路子都有可能。

但一旦“胶水双核”的框架出来了、片内总线、共用cache以及有效性算法等等发展起来了、适应这个架构的高级指令集设计出来了,CPU的发展路径就被固定到现在这个模式上了。


类似的,显卡搞通用计算、做GPGPU深入人心了,那么CPU上面再搞AVX就要受限了——小打小闹你可以靠低延迟获胜;但真玩大的……怎么可能玩的过GPU?

但另一方面,CPU做核芯显卡,直接整合个GPU进去、然后再把GPU计算单元和CPU内部单元无缝融合……就好像80386CPU和80387数学协处理器被整合进486CPU一样,这会不会是未来的发展方向呢?


回到问题:在多核多线程架构完全确立下来之前,你想让编程语言如何支持异步编程呢?

在相关领域的研究/实践足够多、方案足够成熟之前,你连“异步究竟应该做成‘协作式多任务’还是‘抢占式多任务’”都不可能知道——async/await并不是表面看来那样,仅仅是一个简单的关键字;它的背后必需存在一个合理的体系,一个异步执行框架,不然就没法实现功能。


显然,过早的和一个不成熟的方案绑定(比如你的语言特性完全绑定于Windows3.1的协作式模型),只会让你的心血跟着这个不成熟的方案一起付诸东流。

因此,在时代来临之前,在编程语言中添加“异步编程的支持”,显然是有百害而无一利的。

这种东西就应该用库支持。将来架构发展方向变了,也就是废掉一个库的问题,不会拖死一整个语言,对吧。


但到了现在,由于硬件架构发展方向越发清晰、固定,我们终于可以确定“异步编程模型”应该是什么样子了:

1、最上层是进程;进程是持有资源的最小单位

2、中层是线程;线程不持有资源,是CPU调度的最小单位

3、下层是协程;协程既不持有资源、也不必在意CPU调度,它仅仅关注“协作式的、自然的执行流程切换”


当然,细节肯定还是会千变万化的;但大致来说这个整体图景不太可能有大的变动了。


底层稳定下来了,语言的直接支持才可能跟上。

不然的话,你见过哪门编程语言换个新一代的CPU就得禁用若干个关键字、或者把某些关键字的含义改变一番的?

python 2 to 3不过是风格上的少许改变,至今都还鸡飞狗跳的。它要告诉你“因为windows 95上进程的含义和Windows 3.1有所不同,因此你必须检查你的程序,在如下(省略五千字)情况下,请不要使用async关键字”或者“由于zen4改用了超长指令字架构,await在如下(省略一万字)情况下无法正常工作”,你还不得去刨Guido的祖坟啊。


反过来说也对:一旦大部分语言和OS和CPU的某个特性绑定,那么OS和CPU就没法改变设计了。比如,如果现在有人提出了一个比线程/进程模型更优越的新架构,OS/CPU制造商就不得不在“抛弃一部分语言和它们的用户”和“使用新架构但给出一个性能有所损失的兼容层”和“挺好的,但……算了,雪藏起来吧”之间做出选择。


再换句话说:程序设计语言本就应该和OS和CPU的具体实现脱耦;在进程-线程-协程模型确立之前,提供async/await就产生了“与OS或CPU的紧密耦合”,无论对编译器商还是对OS开发者还是CPU制造者,这都是个极大的不利。


注意这是“条件不成熟”,并不是什么“滞后”。

举例来说,AIO至今没有一个统一的方案(Windows仍然坚持自己的‘完成端口’,Linux则继续它的epoll,而BSD觉得kqueue挺好的);因此没有任何语言提供AIO的直接语法支持——你完全可以用诸如libevent之类库写出跨平台的高性能网络服务程序;这样将来倘若有人搞出来一个真正天才的、足以一统天下的方案,你只需提供一个接口兼容的转接层,原本的程序就仍然能运行。

但让某种语言直接提供支持?将来必定是天下大乱:改变语义,原有的项目统统死掉;内部做判断、给新项目用新语义,无论编译器还是程序编写的工作都会变得极其复杂、易错。

想想新的入门者必须区分3.7.13之前和之后的版本,不同版本await关键字含义大体相同但又有微妙的差别……

这也太疯狂太不负责任了,对吧。


RECOMMEND

- 点个在看你最好看 -