一起读 Gevent 源码
admin
2023-07-31 00:47:14
0

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

Greenlet

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

12345678910111213141516 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 在执行(同一线程里)。正如例子中最后三句话,我们创建了 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 示例就是这样的了:

123 import geventgevent.sleep(1) 

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

12345 def sleep(seconds=0):    hub = get_hub()    loop = hub.loop    hub.wait(loop.timer(seconds)) 

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

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

123456789101112 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 实例:

12345678 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 的关键属性——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() 的实现吧。

ng>协程。其实 Greenlet 本身非常简单,其自身实现的功能也非常直接。区别于常规的编程思路——顺序执行、调用进栈、返回出栈—— Greenlet 提供了一种在不同的调用栈之间自由跳跃的功能。从一个简单的例子来看一下吧(摘自官方文档):

12345678910111213141516 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 在执行(同一线程里)。正如例子中最后三句话,我们创建了 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 示例就是这样的了:

123 import geventgevent.sleep(1) 

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

12345 def sleep(seconds=0):    hub = get_hub()    loop = hub.loop    hub.wait(loop.timer(seconds)) 

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

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

123456789101112 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 实例:

12345678 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 的关键属性——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() 的实现吧。

相关内容

热门资讯

Mobi、epub格式电子书如... 在wps里全局设置里有一个文件关联,打开,勾选电子书文件选项就可以了。
500 行 Python 代码... 语法分析器描述了一个句子的语法结构,用来帮助其他的应用进行推理。自然语言引入了很多意外的歧义,以我们...
定时清理删除C:\Progra... C:\Program Files (x86)下面很多scoped_dir开头的文件夹 写个批处理 定...
scoped_dir32_70... 一台虚拟机C盘总是莫名奇妙的空间用完,导致很多软件没法再运行。经过仔细检查发现是C:\Program...
65536是2的几次方 计算2... 65536是2的16次方:65536=2⁶ 65536是256的2次方:65536=256 6553...
小程序支付时提示:appid和... [Q]小程序支付时提示:appid和mch_id不匹配 [A]小程序和微信支付没有进行关联,访问“小...
pycparser 是一个用... `pycparser` 是一个用 Python 编写的 C 语言解析器。它可以用来解析 C 代码并构...
微信小程序使用slider实现... 众所周知哈,微信小程序里面的音频播放是没有进度条的,但最近有个项目呢,客户要求音频要有进度条控制,所...
Apache Doris 2.... 亲爱的社区小伙伴们,我们很高兴地向大家宣布,Apache Doris 2.0.0 版本已于...
python清除字符串里非数字... 本文实例讲述了python清除字符串里非数字字符的方法。分享给大家供大家参考。具体如下: impor...