这一篇主要想跟大家分享一下 Gevent 实现的基础逻辑,也是有同学对这个很感兴趣,所以贴出来跟大家一起分享一下。
Greenlet
我们知道 Gevent 是基于 Greenlet 实现的,greenlet 有的时候也被叫做微线程或者协程。其实 Greenlet 本身非常简单,其自身实现的功能也非常直接。区别于常规的编程思路——顺序执行、调用进栈、返回出栈—— Greenlet 提供了一种在不同的调用栈之间自由跳跃的功能。从一个简单的例子来看一下吧(摘自官方文档):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
from greenlet import greenlet def test1(): print 12 gr2.switch() print 34 def test2(): print 56 gr1.switch() print 78 gr1 = greenlet(test1) gr2 = greenlet(test2) gr1.switch() |
这里,每一个 greenlet
就是一个调用栈——您可以把他想象成一个线程,只不过真正的线程可以并行执行,而同一时刻只能有一个 greenlet
在执行(同一线程里)。正如例子中最后三句话,我们创建了 gr1
和 gr2
两个不同的调用栈空间,入口函数分别是 test1
和 test2
;这最后一句 gr1.switch()
得多解释一点。
因为除了 gr1
和 gr2
,我们还有一个栈空间,也就是所有 Python 程序都得有的默认的栈空间——我们暂且称之为 main
,而这一句 gr1.switch()
恰恰实现了从 main
到 gr1
的跳跃,也就是从当前的栈跳到指定的栈。这时,就犹如常规调用 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
时所在的那个栈,前面的例子中,gr1
和 gr2
都是在 main
里被创建的,所以他们俩的 parent 都是 main
。所以当 gr1
结束的时候,会回到 main
的最后一句,接着 main
结束了,所以整个程序也就结束了——78
从来没有被执行到过。另外,greenlet
的 parent 也可以手工设置。
简单来看,greenlet 只是为 Python 语言增加了创建多条执行序列的功能,而且多条执行序列之间的切换还必须得手动显式调用 switch()
才行;这些都跟异步 I/O 没有必然关系。
gevent.sleep
接着来看 Gevent。最简单的一个 Gevent 示例就是这样的了:
1 2 3 |
import gevent gevent.sleep(1) |
貌似非常简单的一个 sleep
,却包含了 Gevent 的关键结构,让我们仔细看一下 sleep
的实现吧。代码在 gevent/hub.py
:
1 2 3 4 5 |
def sleep(seconds=0): hub = get_hub() loop = hub.loop hub.wait(loop.timer(seconds)) |
这里我把一些当前用不着的代码做了一些清理,只留下了三句关键的代码,其中就有 Gevent 的两个关键的部件——hub
和 loop
。loop
是 Gevent 的核心部件,也就是主循环核心,默认是用 Cython 写的 libev 的包装(所以性能杠杠滴),稍后会在详细提到它。hub
则是一个 greenlet,里面跑着 loop
。
hub
是一个单例,从 get_hub()
的源码就可以看出来:
1 2 3 4 5 6 7 8 9 10 11 12 |
import _thread _threadlocal = _thread._local() def get_hub(*args, **kwargs): global _threadlocal try: return _threadlocal.hub except AttributeError: hubtype = get_hub_class() hub = _threadlocal.hub = hubtype(*args, **kwargs) return hub |
所以第一次执行 get_hub()
的时候,就会创建一个 hub
实例:
1 2 3 4 5 6 7 8 |
class Hub(greenlet): loop_class = config('gevent.core.loop', 'GEVENT_LOOP') def __init__(self): greenlet.__init__(self) loop_class = _import(self.loop_class) self.loop = loop_class() |
同样这是一段精简了的代码,反映了一个 hub
的关键属性——loop
。loop
实例随着 hub
实例的创建而创建,默认的 loop
就是 gevent/core.ppyx
里的 class loop
,也可以通过环境变量 GEVENT_LOOP
来自定义。
值得注意的是,截止到 hub = get_hub()
和 loop = hub.loop
,我们都只是创建了 hub
和 loop
,并没有真正开始跑我们的主循环。稍安勿躁,第三句就要开始了。
loop
有一堆接口,对应着底层 libev 的各个功能,详见此处。我们这里用到的是 timer(seconds)
,该函数返回的是一个 watcher
对象,对应着底层 libev 的 watcher 概念。我们大概能猜到,这个 watcher
对象会在几秒钟之后做一些什么事情,但是具体怎么做,让我们一起看看 hub.wait()
的实现吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
from greenlet import greenlet def test1(): print 12 gr2.switch() print 34 def test2(): print 56 gr1.switch() print 78 gr1 = greenlet(test1) gr2 = greenlet(test2) gr1.switch() |
这里,每一个 greenlet
就是一个调用栈——您可以把他想象成一个线程,只不过真正的线程可以并行执行,而同一时刻只能有一个 greenlet
在执行(同一线程里)。正如例子中最后三句话,我们创建了 gr1
和 gr2
两个不同的调用栈空间,入口函数分别是 test1
和 test2
;这最后一句 gr1.switch()
得多解释一点。
因为除了 gr1
和 gr2
,我们还有一个栈空间,也就是所有 Python 程序都得有的默认的栈空间——我们暂且称之为 main
,而这一句 gr1.switch()
恰恰实现了从 main
到 gr1
的跳跃,也就是从当前的栈跳到指定的栈。这时,就犹如常规调用 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
时所在的那个栈,前面的例子中,gr1
和 gr2
都是在 main
里被创建的,所以他们俩的 parent 都是 main
。所以当 gr1
结束的时候,会回到 main
的最后一句,接着 main
结束了,所以整个程序也就结束了——78
从来没有被执行到过。另外,greenlet
的 parent 也可以手工设置。
简单来看,greenlet 只是为 Python 语言增加了创建多条执行序列的功能,而且多条执行序列之间的切换还必须得手动显式调用 switch()
才行;这些都跟异步 I/O 没有必然关系。
gevent.sleep
接着来看 Gevent。最简单的一个 Gevent 示例就是这样的了:
1 2 3 |
import gevent gevent.sleep(1) |
貌似非常简单的一个 sleep
,却包含了 Gevent 的关键结构,让我们仔细看一下 sleep
的实现吧。代码在 gevent/hub.py
:
1 2 3 4 5 |
def sleep(seconds=0): hub = get_hub() loop = hub.loop hub.wait(loop.timer(seconds)) |
这里我把一些当前用不着的代码做了一些清理,只留下了三句关键的代码,其中就有 Gevent 的两个关键的部件——hub
和 loop
。loop
是 Gevent 的核心部件,也就是主循环核心,默认是用 Cython 写的 libev 的包装(所以性能杠杠滴),稍后会在详细提到它。hub
则是一个 greenlet,里面跑着 loop
。
hub
是一个单例,从 get_hub()
的源码就可以看出来:
1 2 3 4 5 6 7 8 9 10 11 12 |
import _thread _threadlocal = _thread._local() def get_hub(*args, **kwargs): global _threadlocal try: return _threadlocal.hub except AttributeError: hubtype = get_hub_class() hub = _threadlocal.hub = hubtype(*args, **kwargs) return hub |
所以第一次执行 get_hub()
的时候,就会创建一个 hub
实例:
1 2 3 4 5 6 7 8 |
class Hub(greenlet): loop_class = config('gevent.core.loop', 'GEVENT_LOOP') def __init__(self): greenlet.__init__(self) loop_class = _import(self.loop_class) self.loop = loop_class() |
同样这是一段精简了的代码,反映了一个 hub
的关键属性——loop
。loop
实例随着 hub
实例的创建而创建,默认的 loop
就是 gevent/core.ppyx
里的 class loop
,也可以通过环境变量 GEVENT_LOOP
来自定义。
值得注意的是,截止到 hub = get_hub()
和 loop = hub.loop
,我们都只是创建了 hub
和 loop
,并没有真正开始跑我们的主循环。稍安勿躁,第三句就要开始了。
loop
有一堆接口,对应着底层 libev 的各个功能,详见此处。我们这里用到的是 timer(seconds)
,该函数返回的是一个 watcher
对象,对应着底层 libev 的 watcher 概念。我们大概能猜到,这个 watcher
对象会在几秒钟之后做一些什么事情,但是具体怎么做,让我们一起看看 hub.wait()
的实现吧。