pyhton协程-gevent

403 阅读8分钟

1 线程与协程

1.1 关于线程

线程模型
线程的实现模型主要由3种:内核级线程模型、用户级线程模型、混合型线程模型。他们最大的区别在于线程与内核调度实体KSE(Kernel Scheduling Entity)之间的对应关系,可以被操作系统内核调度的对象实体,也称为内核级线程,操作系统内核最小调度单元
  • 内核级线程模型 用户线程和KSE 1:1的关系,大部分编程语言的线程库都是对操作系统的KSE的封装,调度工作完全由OS调度器来做,实现方式简单。
  • 用户级线程模型 用户线程和KSE M:1的关系,这种线程的创建、销毁以及多个线程之间的协调都是由用户自己的线程库负责的,这也就是协程实现的方式。这种方式下,创建线程的数量和上下文切换所消耗的资源代价会很小。但是有缺点:当我们在某个用户线程发起了阻塞的系统调用(阻塞方式read网络IO),导致线程阻塞这种情况下KSE就会发生阻塞,当内核线程发生阻塞那么cpu将会将当前KSE挂起,对应的用户线程全部变为阻塞状态。 要解决这种问题,常见的就是用户线程里做文章,一旦发生了阻塞的操作,就主动让出当前的用户线程,执行下一个线程,从而避免KSE阻塞。类似python的gevent使用时需要打上一个monkey_patch封装IO操作。
  • 混合型线程模型 用户线程和KSE M:N的关系,在一个进程中创建多个KSE,并且线程可以与不同的KSE关联,所以当某一个KSE执行一个阻塞的用户线程的被挂起的时候,与该KSE绑定的其它用户线程将会被分配到其它的KSE,这种动态关联机制时用户实现的,比如go就是实现了该模型,用户调度器实现用户线程到KSE的“调度”,内核调度器实现KSE到CPU上的调度

1.2 协程

英文叫Coroutine,通俗点叫用户态线程,但区别于线程的抢占式调度,使用的是协作调度,从一个状态开始一条执行流在多个协程间切换,好像多个协程写作完成一项任务。

2.python协程

2.1 从生成器到协程

关于生成器,我们知道是使用yield的迭代器具体代码:
def generator():
    for i in range(10):
        yield i
        print 123
a = generator()
next(a)
这个执行过程就是当我们生成一个a的generator时,代码已经执行到yield,程序被夯住,然后调用一次next(a)此时继续执行返回yield后面的i。我们能在通过next(a)来控制它的执行流程,从而实现不同子程序的之间的交替执行。
在python的PEP-342里有对yield做了增强,能够传入值,能够在try-finally里使用等等,是生成器更能被用作协程。
def generator(g2):
    for i in range(10):
        num = yield 1
a = generator()
a.send(None)
a.send(2)

2.2 greenlet

协程,协作式调度的,而不是抢占式调度,greenlet是python的一个C扩展,能够提供自行调度的协程,他比yield协程的好处就是能够在一个协程传递参数给另一个协程(在switch函数中传值,虽然yield也能给协程传值,但是我们是必须在主函数的调度算法里。
def test1():
    print("test1 into")
    gr2.switch()
    print("test1 out")
​
​
def test2():
    print("test2 into")
    gr1.switch()
    print("test2 out")
​
​
gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch()
上面共启动了两个greenlet协程,首先在主线程中通过gr1.switch()切换到test1函数,执行到gr2.switch()我们切换到了函数test2,随后又一次切换到了test1,最后当test1函数退出将直接返回到主线程所以我们能看到的输出就是:
test1 into
test2 into
test1 out
多个greenlet可以共存在一个线程中,但是任意时刻一个线程中只有一个greenlet在运行。
为了让另一个greenlet运行,当前运行的greenlet必须放弃控制权,这叫做切换。换时必须明确选择要让哪个greenlet来接替执行,当前的greenlet里调用目的地greenlet的switch方法来进行这样的切换。当切换发生时,当前greenlet的调用栈会被保存下来,目的地greenlet的调用栈被放到正确的位置,然后执行流在目的地greenlet上次切换的地方继续执行。

2.3 gevent

2.3.1 gevent 概念

一个第三方协程库,基于greenlet实现自己的协程调度机制, 实现的是协作式调度,区别于CPU的抢占式调度。
事件循环
gevent让操作系统传发出事件让事件循环知道何时一个事件发生了,例如数据已经传输过来了
socket可以读了,从而代替阻塞和一直等待socket操作完成(轮询)的方式。通过这样做,
gevent可以继续运行下一个或许已经有事件就绪的greenlet。这样重复的注册事件以及在事件
发生时进行处理的流程就是事件循环了。
hub是gevent中最重要的一部分
当gevent API中的一个函数A要阻塞的时候,它将获得一个gevent.hub.Hub的实例(Hub实
例是一个非常特别的运行着事件循环的greenlet),然后函数A的那个greenlet被切换到
Hub(将控制权交还给Hub)。如果当时还没有gevent.hub.Hub实例,就会自动创建一个

2.3.2 gevent流程

hub本身也是一个greenlet,在hub里我们运行着gevent的核心之一loop也就是事件循环,loop里封装着对应着底层libev的各个接口,当loop运行的时候,它将会用于处理所有的greenlet都可以通过get_hub获取当前的hub对象,若果不存在就重新创建一个。任何一个greenlet切换后都将进入到hub这个greenlet里,如果此时没有开启事件轮训就主动开启,之后便等待下一个greenlet的回调事件。
# _socket3.py
def __init_common(self):
    _socket.socket.setblocking(self._sock, False) # 设置为非阻塞模式的socket
    fileno = _socket.socket.fileno(self._sock) 
    self.hub = get_hub()
    io_class = self.hub.loop.io # 预定义一个 io watcher
    self._read_event = io_class(fileno, 1) # socket的可读事件的 watcher
    self._write_event = io_class(fileno, 2)# socket的可写事件的 watcher
    self.timeout = _socket.getdefaulttimeout()
其中get_hub获取hub对象
def get_hub_noargs():
    hub = _threadlocal.hub
    if hub is None:
        hubtype = get_hub_class()
        # 这里的hub是一个线程全局共享变量
        # hubtype在最开始会设置成gevent.hub.Hub
        hub = _threadlocal.hub = hubtype()
    return hub
在python的标准库里socket默认是阻塞模式,这种模式下它的许多函数都是阻塞的,其中就包括recv(),接收对端发送的数据。
    def recv(self, *args):
        while True:
            try:
                return self._sock.recv(*args)
            except error as ex:
                # 非阻塞模式下,如果没有数据将会抛出EWOULDBLOCK
                if ex.args[0] != EWOULDBLOCK or self.timeout == 0.0:
                    raise
            # self._read_event是一个io watcher 监听的是读事件
            self._wait(self._read_event)
转了一堆弯找到最终调用的是hub.wait(),首先声明一个Waiter对象,将waiter.switch函数传入到绑定了当前socket读事件的io watcher,当watcher监听到有数据到来时,就执行回调函数waiter.switch
    def wait(self, watcher):
        waiter = Waiter(self) # pylint:disable=undefined-variable
        watcher.start(waiter.switch, waiter)
        try:
            result = waiter.get()
            if result is not waiter:
                raise xxxx
        finally:
            watcher.stop()
到现在我们还不知道,在当前greenlet切出进hub中,到最后socket收到对端发送来消息,watcher回调成功切入到当前greenlet这段时间里,究竟gevent在干嘛。这个问题先放着,我们去探究另一个问题。
hub是怎么管理的greenlet的
我们可以从一个gevent的使用例子来看
def do_something():
    pass
g1 = gevent.spawn(do_something)
g1.join()
看函数名,spawn就是产生一个什么东西,那现在用的是gevent那大概率是生产一个协程
def spawn(cls, *args, **kwargs):
    g = cls(*args, **kwargs)
    g.start()
    return g
看代码,我们发现就是初始化了一个Greenlet(继承自greenlet类,后者为c库greenlet接口的封装)实例,然后调
用start最后返回它。那我们知道,我们首先去看这个类的初始化方法,发现有这么一行
_greenlet__init__(self, None, get_hub())
def __init__(self, run=None, parent=None): 
    pass
这里的parent,参数就是我们的hub,没错,在同一线程内生成一个协程,都会指定它的parent为同一个hub,这样当协程执行完退出时就不会退出到主线程,而是会跳到我们的hub协程。
我们再看下start方法
def start(self):
    """Schedule the greenlet to run in this loop iteration"""
    if self._start_event is None:
        _call_spawn_callbacks(self)
        hub = get_my_hub(self)  # 获取当前的parent协程,也就是hub
        self._start_event = hub.loop.run_callback(self.switch) # 注册到hub的事件循环中
到此也只是注册两个协程,并没有实际运行,我们再看下 join(),简化代码我们看到这段。
到此也只是注册两个协程,并没有实际运行,我们再看下 join(),简化代码我们看到这段。
def join():
    result = get_my_hub(self).switch()
其实这就是切换到hub这个greenlet,正如switch这个方法定义的:
If this greenlet has never been run, then this greenlet
will be switched to using the body of self.run(*args, **kwargs).
我们去看下hub的self.run干了些什么,简化一下
 def run(self):       
     while 1:
        loop = self.loop
        try:
            loop.run()
        finally:
            loop.error_handler = None  # break the refcount cycle
大概逻辑就是执行我们事件循环 loop,从Hub出发去寻找Loop最终代码落入到gevent.libev.corecffi.loop(如果是linux环境)再往里就调用c的lib库了,具体事件循环对象暴露哪些接口我们可以看gevent._interfaces.ILoop, loop.run()做的执行底层livev的事件监听,当有事件被触发,就会执行当时注册的回调函数waiter.switch这样就切换回指定的协程。

2.4 asyncio

由python创始人亲自操刀的异步编程标准库,核心组件除了 事件循环、Coroutine 还有任务(Task)、未来对象(Future)。