一起读 Gevent 源码

427 查看

这一篇主要想跟大家分享一下 Gevent 实现的基础逻辑,也是有同学对这个很感兴趣,所以贴出来跟大家一起分享一下。

Greenlet

我们知道 Gevent 是基于 Greenlet 实现的,greenlet 有的时候也被叫做微线程或者协程。其实 Greenlet 本身非常简单,其自身实现的功能也非常直接。区别于常规的编程思路——顺序执行、调用进栈、返回出栈—— Greenlet 提供了一种在不同的调用栈之间自由跳跃的功能。从一个简单的例子来看一下吧(摘自官方文档):

这里,每一个 greenlet 就是一个调用栈——您可以把他想象成一个线程,只不过真正的线程可以并行执行,而同一时刻只能有一个 greenlet 在执行(同一线程里)。正如例子中最后三句话,我们创建了 gr1gr2 两个不同的调用栈空间,入口函数分别是 test1test2;这最后一句 gr1.switch() 得多解释一点。

因为除了 gr1gr2,我们还有一个栈空间,也就是所有 Python 程序都得有的默认的栈空间——我们暂且称之为 main,而这一句 gr1.switch() 恰恰实现了从 maingr1 的跳跃,也就是从当前的栈跳到指定的栈。这时,就犹如常规调用 test1() 一样,gr1.switch() 的调用暂时不会返回结果,程序会跳转到 test1 继续执行;只不过区别于普通函数调用时 test1() 会向当前栈压栈,而 gr1.switch() 则会将当前栈存档,替换成 gr1 的栈。如图所示:

对于这种栈的切换,我们有时也称之为执行权的转移,或者说 main 交出了执行权,同时 gr1 获得了执行权。Greenlet 在底层是用汇编实现的这样的切换:把当前的栈(main)相关的寄存器啊什么的保存到内存里,然后把原本保存在内存里的 gr1 的相关信息恢复到寄存器里。这种操作速度非常快,比操作系统对多进程调度的上下文切换还要快。代码在这里,有兴趣的同学可以一起研究一下(其中 switch_x32_unix.h 是我写的哈哈)。

回到前面的例子,最后一句 gr1.switch() 调用将执行点跳到了 gr1 的第一句,于是输出了 12。随后顺序执行到 gr2.switch(),继而跳转到 gr2 的第一句,于是输出了 56。接着又是 gr1.switch(),跳回到 gr1,从之前跳出的地方继续——对 gr1 而言就是 gr2.switch() 的调用返回了结果 None,然后输出 34

这个时候 test1 执行到头了,gr1 的栈里面空了。Greenlet 设计了 parent greenlet 的概念,就是说,当一个 greenlet 的入口函数执行完之后,会自动切换回其 parent。默认情况下,greenlet 的 parent 就是创建该 greenlet 时所在的那个栈,前面的例子中,gr1gr2 都是在 main 里被创建的,所以他们俩的 parent 都是 main。所以当 gr1 结束的时候,会回到 main 的最后一句,接着 main 结束了,所以整个程序也就结束了——78 从来没有被执行到过。另外,greenlet 的 parent 也可以手工设置。

简单来看,greenlet 只是为 Python 语言增加了创建多条执行序列的功能,而且多条执行序列之间的切换还必须得手动显式调用 switch() 才行;这些都跟异步 I/O 没有必然关系。

gevent.sleep

接着来看 Gevent。最简单的一个 Gevent 示例就是这样的了:

貌似非常简单的一个 sleep,却包含了 Gevent 的关键结构,让我们仔细看一下 sleep 的实现吧。代码在 gevent/hub.py

这里我把一些当前用不着的代码做了一些清理,只留下了三句关键的代码,其中就有 Gevent 的两个关键的部件——hublooploop 是 Gevent 的核心部件,也就是主循环核心,默认是用 Cython 写的 libev 的包装(所以性能杠杠滴),稍后会在详细提到它。hub 则是一个 greenlet,里面跑着 loop

hub 是一个单例,从 get_hub() 的源码就可以看出来:

所以第一次执行 get_hub() 的时候,就会创建一个 hub 实例:

同样这是一段精简了的代码,反映了一个 hub 的关键属性——looploop 实例随着 hub 实例的创建而创建,默认的 loop 就是 gevent/core.ppyx 里的 class loop,也可以通过环境变量 GEVENT_LOOP 来自定义。

值得注意的是,截止到 hub = get_hub()loop = hub.loop,我们都只是创建了 hubloop,并没有真正开始跑我们的主循环。稍安勿躁,第三句就要开始了。

loop 有一堆接口,对应着底层 libev 的各个功能,详见此处。我们这里用到的是 timer(seconds),该函数返回的是一个 watcher 对象,对应着底层 libev 的 watcher 概念。我们大概能猜到,这个 watcher 对象会在几秒钟之后做一些什么事情,但是具体怎么做,让我们一起看看 hub.wait() 的实现吧。