python gevent原理

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中的两个核心部件,hubloop

  • hub是一个greenlet,里面跑loophub是一个单例,第一次执行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。

参考文献

Was this helpful?

0 / 0

发表回复 0