关于协程道与术的区别,我沉默了:梅尔文编译器

协程概念的诞生

我们先抛出一个肤浅的结论:协程是一个广义的设计概念,我们经常谈论具体的实现。

如果你很好地理解了这个想法,那么技术点就很简单了。关于协程之路和技术的区别:

古神器COBOL

协程的概念比线程更早出现,甚至可以追溯到 1950 年代。提到协程,就不得不提COBOL这个最早的具有强大生命力的高级编程语言。

起初我以为 COBOL 早就从历史上消失了,但我错了。

COBOL是一种面向过程的高级编程语言能让线程停止执行的有,主要用于数据处理,是世界上使用最广泛的高级语言。COBOL是English Common Business-Oriented Language的缩写,本意是商务通用语言。

截至今年,全球约有 1w 台大型机,3.8w+ COBOL 编写的遗留系统代码约 2000 亿行,占比高达 65%。它基于COBOL,影响巨大。

早在 1958 年,美国计算机科学家 Melvin Conway 就开始研究基于磁带存储的 COBOL 的编译器优化问题。这在当时是一个非常热门的话题,很多年轻人才涌入其中。包括图灵奖得主唐纳德·欧文·克努斯(Donald Ervin Knuth)也写了一个优化编译器。

看着这两人的简介,我沉默了:

梅尔文·康威(Melvin Conway)也是一位超级boss,著名的康威定律提出者。

Donald Irwin Knuth 是算法和编程技术的先驱,1974 年图灵奖得主,计算机排版系统 TeX 和字体设计系统 METAFONT 的发明者,他深受这些成就的影响和极大的创造力。 《计算机程序设计艺术》被美国科学家杂志列为 20 世纪最重要的 12 部物理科学专着之一。

那么到底是什么问题让这群天才们投入了这么多精力呢?来看看!

COBOL 编译器的技术难题

我们都知道高级编程语言需要编译器来生成二进制可执行文件。编译器的基本步骤包括:读取字符流、词法分析、语法分析、语义分析、代码生成器、代码优化器等。

在这种流水线式的过程中,上一步的输出作为下一步的输入,中间结果可以存储在内存中。这对现代计算机来说是没有压力的,但受软硬件水平的限制。在几十年前的 COBOL 中,语言很困难。

1958年,那个时候存储还没有发展起来,1951年计算机中使用磁带作为内存,所以那个时代的COBOL非常依赖磁带。

其实我在网上找了很多资料看当时编译器出了什么问题,只找到了一个:编译器无法读取一次磁带来完成整个编译过程,也就是这样——所谓的one-pass编译器还没有产生。

那时COBOL程序是写在磁带上的,磁带不支持随机读写,只能顺序读取,而且当时的内存不能把整个磁带内容都放进去,所以是需要在编译前再次读写。从头开始阅读。

所以,我想出了 COBOL 编译器和磁带之间两种可能的多通道交互:

可能性二

听过磁带的朋友一定知道磁带的两个基本操作:倒带和快进。

在完成编译器的词法分析和句法分析时,磁带需要反复倒带和快进才能找到两类分析所需的部分。类似于磁盘的寻道,磁头需要反复移动和跳跃,目前的磁带不一定支持随机读写。

从一些资料可以看出,当时COBOL编译器的各个方面都是相互独立的,而这种软硬件的综合限制,使得无法实现一次性编译。

协作解决方案

在 Melvin Conway 的编译器设计中,词法分析和语法分析是一起工作的,而不是像其他编译器那样相互独立,两个模块是交错的,编译器的控制流在词法分析和语法分析之间。在以下之间来回切换:

可以看出,这个方案的核心思想是:

Melvin Conway 构建的这种协同工作机制要求参与者在让出控制流时记住自己的状态,这样当控制流返回时,他们可以从上次让出的位置恢复执行。. 简而言之,协程的整个精神就是控制流的主动屈服和恢复。

这种协作任务流程与计算机中断非常相似。在当时条件的约束下,Melvin Conway 提出的 give/restore 模型的协同程序被认为是最早的协程概念,基于这种思想可以创造新的 COBOL 编译器。

1963年,梅尔文·康威也发表了一篇论文来说明他自己的这个想法。虽然已经过去了半个多世纪,但我还是有幸找到了这篇论文:

说实话,这篇论文真的有点难,时间太长了,很难引起共鸣,最后我放弃了,不然说不定能看懂之前编译器的具体问题。

被低估的协程

虽然协程的概念比线程出现得更早,但是协程一直没有上过台,实在是有点赶。

我们在学校的时候,老师讲了一些软件设计思想,其中主流语言都提倡自顶向下的编程思想:

分解要完成的任务,首先在最高层定义、设计、编程和测试问题,将未解决的问题作为子任务放到下一层去解决。

这样,一层一层地定义、设计、编程、测试,直到所有层的问题都被实用程序解决,就可以设计出一个层次结构的程序。

C语言是典型的top-down思想的代表。以main函数为入口能让线程停止执行的有,各个模块依次形成层次调用关系。同时,每个模块都有下级子模块,子模块也有层次调用关系。

但是,协程调度的思想不同于自顶向下。协程中模块之间存在较大的耦合关系,不符合高内聚、低耦合的编程思想。相比之下,自顶向下使程序结构清晰,层次调度清晰,代码可读性和可维护性非常好。

与线程相比,协作任务系统允许调用者决定何时放弃,这比操作系统的抢占式调度成本要低得多,在切换线程以恢复场景时节省了大量时间。状态,并且会非常频繁地切换,这会消耗更多的资源。

总而言之,协程完全是用户模式的行为。程序员决定何时放弃控制。用于保存场景和切换恢复的资源也很少,也完全符合提高处理器的效率。

所以我不禁要问:协程看起来不错,为什么还没有成为主流?

换句话说:能做协程的线程也做的很好,而线程做不好的地方,用户暂时不需要,所以协程就这么被低估了。

事实上,虽然协程在 x86 架构上并没有造成太大的麻烦,但由于抢占式任务系统依赖于 CPU 硬件的支持,所以对硬件的要求比较高。对于一些嵌入式设备,协同调度更适合,所以程协程也在另外一个领域大展拳脚。

协程的兴起

我们的 CPU 挤压从未停止。

对于 CPU,任务分为两大类:计算密集型和 IO 密集型。

计算密集型已经可以最大限度发挥CPU的作用,但IO密集型一直是提高CPU利用率的难点。

IO密集型任务之痛

对于IO密集型任务,抢占式调度中也有相应的解决方案:异步+回调。

也就是说,当IO被阻塞时,比如下载图片时会立即返回,等待下载完成后回调结果并传递给发起者。

就像你经常去早餐店一样,油条还没做好。你和老板很熟,付了钱去座位上玩手机。当你的油条做好后,服务员会端过来。这是典型的异步+回调。

异步+回调虽然在现实生活中看起来很简单,但在程序设计中却是一件令人头疼的事情。在某些场景下,整个程序的可读性很差,写起来也不容易。相反,同步IO虽然效率低,但是写起来很容易,

或者以异步图片下载为例。影像服务中心提供异步接口,发起者请求后立即返回。这时候,影像服务会给发起者一个唯一的ID。影像服务完成下载后,将结果放入消息队列中。,此时发起方需要不断的消费这个MQ才能得到下载结果。

与同步IO相比,整个过程分为几个部分,每个子部分都有状态迁移。对大多数程序员来说,维护状态是一场噩梦,未来将是 bug 的高发地。

用户模式协调调度

随着网络技术的发展和高并发要求,IO类任务处理的抢占式调度效率低下逐渐被重视,协程的机会终于来了。

协程为程序员提供了 IO 处理能力。当 IO 被阻塞时,它会将控制权交给其他协程,等其他协程处理完后再将控制权交还给其他协程。

多个通过yield传递执行权的协程之间的关系不是调用者和被调用者的关系,而是平等、对称、相互协作的关系。

协程之所以没有盛行,除了设计思路上的矛盾之外,还有一些其他的原因,毕竟协程不是灵丹妙药,我们来看看协程到底哪里出了问题:

综上所述,协程和线程并不矛盾。协程的强大之处在于对 IO 的处理,而这恰好是线程的弱点。只有由对立转为合作,才能开辟新局面。

拥抱协程的编程语言

网络操作、文件操作、数据库操作、消息队列操作等重IO操作是任何高级编程语言都无法回避的问题,也是提高程序效率的关键。

Java、C/C++、Python等老牌语言也开始使用第三方包来支持协程,解决自己语言的不足。

像 Golang 这样的新玩家在语言层面原生支持协程,可以说完全拥抱协程,这也造就了 Go 的高并发能力。

下面我们来看看它们是如何实现协程的,以及实现协程的关键点是什么。

Python

Python对协程的支持也经历了多个版本,从部分支持演变为完全支持:

我们使用最新的 async/await 来说明 Python 的协程是如何使用的:

导入异步

从路径库导入路径

导入日志

从 urllib.request 导入 urlopen,请求

导入操作系统

从时间进口时间

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论