在python里面,协程其实没什么可讲的,它就是生成器。
但是关于协程编程却是值得一谈。
不知道从什么时候开始,编程领域开始流行协程编程。普遍上的定义是说协程是用户空间的线程,也叫纤程,轻量级的线程。它像线程一样,有自己的执行入口和调用栈空间,但不同于线程,它对操作系统核心是不可见的,对它的调度需要用户代码自己去实现。
作者最早接触协程是在c语言里,它的一个典型实现是使用makecontext来为协程创建一个执行上下文,通过调用它为协程指定一个栈(堆上申请的一块内存,一般指定1Mb大小)和一个执行的入口(一个函数,就像线程的入口函数一样),然后我们通过swapcontext来启动协程,后面的上下文切换(cpu执行权切换)通过sigsetjmp/siglongjmp来实现。
python里的协程就是生成器了,不同于c语言的实现,python里的协程不需要为它单独指定一个调用栈,而是解释器为每个线程维护唯一一个调用栈。python里的协程执行权切换是通过类似于函数的调用/返回来实现的,调用一个协程(next/send/throw)跟调用一个函数差不多,从协程yield back到它的调用者跟函数返回差不多。
为什么在python里面协程的执行权切换被实现为类似于函数的调用返回?
让我们来研究一下协程切换/交换这回事。coroutine a交换到coroutine b,我们以一个二元关系(coroutine a, coroutine b)来表示。任意的两个协程coroutine a和coroutine b,如果coroutine a不是由coroutine b交换而来,它可能是第一个执行的coroutine,也可能是由另一个coroutine c交换而来,我们把coroutine a交换到coroutine b定义为ENTER,如果coroutine a是由coroutine b交换而来,那么coroutine a再交换到coroutine b,我们把它定义为YIELD。那么,任意的(coroutine a, coroutine b),它要么表示coroutine a ENTER to coroutine b,要么表示coroutine a YIELD back to coroutine b。coroutine的交换还要满足一个条件(记为A),如果coroutine a ENTER to coroutine b,coroutine a恢复执行必须等coroutine b YIELD back to coroutine a。 所以ENTER/YIELD和函数的调用/返回从交换的角度看是等价的,逻辑是一样的。所以协程的上下文切换和函数的调用返回从逻辑上看是一样的。线程的上下文切换就没有这样的因果关系了,系统核心实现了一套算法去决定什么时候暂停一个线程的执行,什么时候去恢复一个线程的执行。
满足条件A的协程的上下文切换就相当于函数的调用返回,基本上不管什么语言的协程上下文切换都满足条件A,作者还没见过不满足的,如果不满足那意味着协程的交换可以出现了一个回路,作者想象不出来什么样的逻辑需要如此实现。
这篇短文是个纲领性的文章,以后,作者会从代码实现的角度重点分析一个协程的编程框架asyncio,敬请期待。