python中的gevent基于GreenLet实现。区别于顺序执行->调用进栈->返回出栈,Greenlet提供了一种在不同的调用栈之间自由跳跃的功能。
从gevent.sleep()的实现讲起
最简单的gevent的示例如下:
import gevent
gevent.sleep(1)
下面,我们以上述代码为例,分析sleep的实现过程。
sleep的实现在代码gevent/hub.py
中,关键代码如下:
def sleep(seconds=0):
hub = get_hub()
loop = hub.loop
hub.wait(loop.timer(seconds))
Gevent中的两个核心部件,hub
与loop
:
hub
是一个greenlet
,里面跑loop
:hub
是一个单例,第一次执行get_hub()
时创建hub实例。源码如下:
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
Hub类的精简代码如下:
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()
可见,默认的loop
就是gevent/core.ppyx
中的class loop
。
loop
是Gevent的主循环核心,其有一堆接口,对应着底层libev的各个功能 详见此处,在gevent.sleep()
的源码实现中,我们使用了timer(seconds)
,该函数返回一个watcher
对象。函数返回的是一个 watcher 对象,对应着底层 libev 的 watcher 概念。watcher对象传给了hub.wait()
.
hub.wait()
的代码如下:
def wait(self, watcher):
waiter = Waiter()
watcher.start(waiter.switch)
waiter.get()
这里使用Waiter()
这个类,其定义如下:
from greenlet import getcurrent
class Waiter(object):
def __init__(self):
self.hub = get_hub()
self.greenlet = None
def switch(self):
assert getcurrent() is self.hub
self.greenlet.switch()
def get(self):
assert self.greenlet is None
self.greenlet = getcurrent()
try:
self.hub.switch()
finally:
self.greenlet = None
这里同样删掉了大量干扰因素。根据前面 wait() 的定义,我们会先创建一个 waiter,然后调用其 get(),随后几秒钟之后 loop 会调用其 switch()。一个个看。
get() 一上来会保证自己不会被同时调用到(assert),接着就去获取了当前的 greenlet,也就是调用 get() 时所处的栈,一直往前找,找到 sleep(1),所以 getcurrent() 的结果是 main。Waiter 随后将 main 保存在了 self.greenlet 引用中。
下面的一句话是重中之重了,self.hub.switch()!由不管任何上下文中,直接往 hub 里跳。由于这是第一次跳进 hub 里,所以此时 loop 就开始运转了。
**正巧,我们之前已经通过 loop.timer(1) 和 watcher.start(waiter.switch),在 loop 里注册了说,1 秒钟之后去调用 waiter.switch,loop 一旦跑起来就会严格执行之前注册的命令。所以呢,一秒钟之后,我们在 hub 的栈中,调用到了 Waiter.switch()。
在 switch() 里,程序一上来就要验证当前上下文必须得是 hub,翻阅一下前面的代码,这个是必然的。最后,跳到 self.greenlet!还记得它被设置成什么了吗?——main。于是乎,我们就回到了最初的代码里,gevent.sleep(1) 在经过了 1 秒钟的等待之后终于返回了。**
回头看一下这个过程,其实也很简单的:当我们需要等待一个事件发生时——比如需要等待 1 秒钟的计时器事件,我们就把当前的执行栈跟这个事件做一个绑定(watcher.start(waiter.switch)),然后把执行权交给 hub;hub 则会在事件发生后,根据注册的记录尽快回到原来的断点继续执行。
异步
hub 一旦拿到执行权,就可以做很多事情了,比如切换到别的 greenlet 去执行一些其他的任务,直到这些 greenlet 又主动把执行权交回给 hub。
任务 greenlet 则在执行至下一次断点时,主动切换回 hub
例如:
import gevent
def beep(interval):
while True:
print("Beep %s" % interval)
gevent.sleep(interval)
for i in range(10):
gevent.spawn(beep, i)
beep(20)
例子里我们总共创建了 10 个 greenlet,每一个都会按照不同频率输出“蜂鸣”;最后一句的 beep(20) 又让 main greenlet 也不断地蜂鸣。算上 hub,这个例子一共会有 12 个不同的 greenlet 在协作式地运行。
IO异步
Gevent 最主要的功能当然是异步 I/O 了。其实,I/O 跟前面 sleep 的例子没什么本质的区别,只不过 sleep 用的 watcher 是 timer,而 I/O 用到的 watcher 是 io。比如说 wait_read(fileno) 是这样的
def wait_read(fileno):
hub = get_hub()
io = hub.loop.io(fileno, 1)
return hub.wait(io)
libev 是依赖操作系统底层的异步 I/O 接口实现的,Linux 用的是 epoll,FreeBSD 则是 kqueue。Python 代码里,socket 会创建一堆 io watcher,对应底层则是将一堆文件描述符添加到一个——比如—— epoll 的句柄里。当切换到 hub 之后,libev 会调用底层的 epoll_wait 来等待这些 socket 中可能出现的事件。一旦有事件产生(可能是一次出现好多个事件),libev 就会按照优先级依次调用每个事件的回调函数。注意,epoll_wait 是有超时的,所以一些无法以文件描述符的形式存在的事件也可以有机会被触发。
Gevent的性能调优
关于 pool 的大小,我觉得是可以算出来的:
1、在压力较小、pool 资源充足的情况下,测得单个请求平均处理总时间,记作 Ta
2、根据系统需求,估计一下能接受的最慢的请求处理时间,记作 Tm
3、设 Ta 中有 Ts 的时间,执行权是不属于当前处理中的 greenlet 的,比如正在进行异步的数据库访问或是调用远端 API 等后端访问
4、在常规压力下,通过测量后端访问请求处理的平均时间,根据代码实际调用情况测算出 Ts
5、pool 的大小 = (Tm / (Ta – Ts)) * 150%,这里的 150% 是个 buffer 值,拍脑门拍出来的
比如理想情况下平均每个请求处理需要 20ms,其中平均有 15ms 是花在数据库访问上(假设数据库性能较为稳定,能够线性 scale)。如果最大能容忍的请求处理时间是 500ms 的话,那池子大小应该设置成 (500 / (20 – 15)) * 150% = 150,也就意味着单进程最大并发量是 150。
从这个算法也可以看出,花在 Python 端的 CPU 时间越少,系统并发量就越高,而花在后端访问上的时间长短对并发影响不是很大——当然了,依然得假设数据库等后端可以线性 scale。
参考文献
- 一起读Gevent源码–Gevent原理
- gevent调度流程解析
- gevent 性能和 gevent.loop 的运用和带来的思考
- Python协程库gevent学习-源码学习(一)
- Python协程库gevent学习-源码学习(二)
Was this helpful?
0 / 0