2.1 协程究竟是什么

Kotlin的协程从Kotlin 1.1实验版(Experimental)到现在,已经非常成熟了,但大家对它的概念却一直存在各种疑问。长期以来,业界对协程的概念一直没有清晰统一的界定,Lua之父Roberto Ierusalimschy在论文“Revisiting Coroutines”中提到,协程鲜见于早期语言实现,究其原因,部分即源于此(见图2-1)。

图2-1 “Revisiting Coroutines”中对于协程概念的讨论

更有意思的是,在查阅资料的过程中,你经常会陷入似懂非懂的状态:觉得别人说的都挺对,可一旦想用就不知如何下手了。这很正常,因为在统一的标准出现之前,大家各有各的理解,不能说谁对谁错,只能说在细节上各有千秋。

显然,这对初学者不太友好,毕竟概念不清晰会让人摸不着头脑。我们看到的大都是不同语言对于协程的实现或者衍生,而不是一个确定的定义,这对后续学习来说有很大困难,因为在很难界定一个东西“是什么”的时候,自然很难进入知识获取过程中的“为什么”和“怎么办”这两个后续环节了。

延伸 类似的例子还有早期的JavaScript。各家在对JavaScript的支持上也是随心所欲,直到ECMAScript标准出现并被广泛支持,情况才稍有好转。而我们熟悉的Java从一开始就“根红苗正”,虽然虚拟机有不同的实现,但也都需要符合虚拟机规范,就连不符合Java虚拟机规范的Android虚拟机Dalvik、Art,也至少在运行Java代码时让开发者感受不到明显的差异。因此JavaScript的书通常要花大量篇幅来介绍JavaScript是什么,而Java的书通常只需要告诉你它最初被称为Oak,因为这个名字被抢注才更名为Java的。

问题的关键在于,协程的概念真的不清晰吗?并非如此,协程的概念最核心的点就是函数或者一段程序能够被挂起,稍后再在挂起的位置恢复。挂起和恢复是开发者的程序逻辑自己控制的,协程是通过主动挂起出让运行权来实现协作的,因此它本质上就是在讨论程序控制流程的机制,这是最核心的点,任何场景下探讨协程都能落脚到挂起恢复

协程与线程最大的区别在于,从任务的角度来看,线程一旦开始执行就不会暂停,直到任务结束,这个过程都是连续的。线程之间是抢占式的调度,因此不存在协作问题。

我们再来理一理协程的概念。

·挂起恢复。

·程序自己处理挂起恢复。

·程序自己处理挂起恢复来实现程序执行流程的协作调度。

相比之下,主流操作系统都有成熟的线程模型,应用层经常提到的线程的概念大多是对应于内核线程的,所以不同的编程语言一旦引入了线程,那么基本上就是照搬了内核线程的概念。线程本身也不是它们实现的——这很好理解,因为线程调度需要由操作系统控制。

延伸 Java对线程提供了很好的支持,这也是Java在高并发场景风生水起的一个关键支柱。如果你有兴趣,可以看看虚拟机底层对线程的支持,例如Android虚拟机,其实就是pthread。Java的Object还有一个wait方法,它几乎支撑了各种锁的实现,其底层是condition。

绝大多数协程都是语言层面自己实现,不同的编程语言有不同的使用场景,自然在实现上也看似有很大的差异。有的语言甚至没有实现协程,但开发者可以通过第三方框架提供协程的能力,例如Java的协程框架Quasar(https://docs.paralleluniverse.co/quasar),因此虽然协程的理论上看起来很简单,但实现上却呈现出多种多样的局面。