gevent初探
0x00 前言
有很多Python语言的协程库,如:tornado、asyncio等。这些库在使用时需要使用特定的语法,如:async/await,对于非协程的代码需要改写才能以协程方式运行。
gevent是一个特殊的协程库,它可以将非协程代码以协程方式运行,从而起到提升性能的作用。本文尝试分析一下它的实现原理。
0x01 使用案例
先看以下这段代码:
1 | import ctypes |
输出内容如下:
1 | I'm thread 1: 140540774946560 32347 |
在这段代码前面加上以下代码:
1 | from gevent import monkey |
输出如下:
1 | I'm thread 1: 21069936 31623 |
可以看出,在加入gevent后,输出与之前有些不同,最大的区别是:两个线程具有相同的线程ID。也就是说,这两个线程其实是跑在同一个线程里的。
还有一点需要注意的地方:Main thread sleep这句输出在前后两次执行是不同的。
0x02 原理分析
来看下patch_thread函数的实现:
1 | def patch_thread(threading=True, _threading_local=True, Event=True, logging=True, |
首先,orig_current_thread变量存储了当前的线程对象,然后调用_patch_module函数patch了threading和thread模块。
_patch_module函数相关实现如下:
1 | def __call_module_hook(gevent_module, name, module, items, _warnings): |
这里比较关键的逻辑有两个:
调用
__call_module_hook执行目标模块中的_gevent_will_monkey_patch和_gevent_did_monkey_patch方法调用
patch_item将原始模块中的指定方法替换为gevent对应模块中的同名方法
可以看出,patch的关键逻辑就是由patch_item实现的,具体要patch的对象是由gevent模块中的__implements__列表指定的。
0x03 threading模块patch分析
对于threading模块,对应模块就是gevent.threading,里面实现了_gevent_will_monkey_patch方法,在hook前做一些准备工作。
1 | import threading as __threading__ |
在gevent.threading模块中要patch的列表是:
1 | __implements__ = [ |
而在gevent.thread模块中则是:
1 | __implements__ = [ |
我们来看下threading模块在启动线程过程中做了哪些操作。
1 | +------------------------+ |
上面的threading._start_new_thread和thread.start_new_thread都是在gevent的__implements__列表中的。也就是说:这两个函数都被gevent hook了。
1 | from gevent.thread import start_new_thread as _start_new_thread |
而由于gevent.threading._start_new_thread其实就是gevent.thread.start_new_thread,所以它们其实是同一个函数。
1 | def start_new_thread(function, args=(), kwargs=None): |
从start_new_thread函数可以看出,启动线程逻辑已经被替换成了greenlet的实现,这也是为什么使用了patch_thread后,真实线程id相同的原因。
0x04 总结
gevent使用动态patch的方法,实现了动态将非协程库变成协程库的功能,在极少修改代码的前提下提升了程序的性能。但是,在上面线程的例子中可以看出,patch后程序的行为可能会有一些差异,所以在使用上还是需要小心。