greenlet、gevent:历史悠久的用于处理并发的模块

greenlet介绍

greenlet是用C语言编写的一个模块,而后让python调用,目的就是为了让python支持协程。python

A “greenlet” is a small independent pseudo-thread. Think about it as a small stack of frames; the outermost (bottom) frame is the initial function you called, and the innermost frame is the one in which the greenlet is currently paused. You work with greenlets by creating a number of such stacks and jumping execution between them. Jumps are never implicit: a greenlet must choose to jump to another greenlet, which will cause the former to suspend and the latter to resume where it was suspended. Jumping between greenlets is called “switching”.网络

When you create a greenlet, it gets an initially empty stack; when you first switch to it, it starts to run a specified function, which may call other functions, switch out of the greenlet, etc. When eventually the outermost function finishes its execution, the greenlet’s stack becomes empty again and the greenlet is “dead”. Greenlets can also die of an uncaught exception.并发

一个"greenlet"是一个小型、独立的伪线程。能够把它想象成一个小型的栈帧,栈底是函数初始调用的位置,栈顶是当前greenlet暂停的位置。你可使用greenlet建立一堆这样的堆栈,而后在它们之间切换执行。跳转不能是隐式的,必需要显式地指定要跳转到另外一个greenlet,这就会使得前一个挂起,后一个从其以前挂起的地方恢复执行。不一样greenlet之间的切换,咱们称之为"switching"框架

当你建立一个greenlet,它将获得一个初始化为空的栈。当你第一次切换到它,它会开始运行指定的函数,或许这个函数还会调用其余的函数、切换跳出greenlet等等,当最终栈底函数执行完毕出栈的时候,这个greenlet的栈再次变为空、"死掉"了。同时greenlet也会由于未捕捉的异常而死掉。socket

from greenlet import greenlet


def foo():
    print("foo start")
    gr2.switch()
    print("foo end")


def bar():
    print("bar start")
    gr1.switch()
    print("bar end")    


gr1 = greenlet(foo)
gr2 = greenlet(bar)

# 以上至关于建立了gr一、gr2两个greenlet
# 可是咱们说了,这至关于建立两个初始化为空的栈,只有当你切换到它,才会运行指定的函数。

# 调用switch表示切换
gr1.switch()

"""
foo start
bar start
foo end
"""

经过上面代码咱们来看一下流程:async

  • 定义两个函数,并建立两个greenlet,此时栈为空
  • 一旦进行switch,便会执行相应的函数。好比这里的gr1.switch(),便会执行foo函数
  • print以后,gr2.switch(),表示切换到另外一个greenlet。所以会先将当前函数执行的上下文、以及CPU寄存器的内容表示起来。
  • 当执行bar函数,print以后咱们又切换到了gr1,那么会经过上下文从暂停的地方恢复执行。
  • print以后这个函数就完全的执行完毕了,那么会出栈,这个greenlet就死掉了,所以bar函数里面的bar end是不会被打印的

所以咱们能够认识到,greenlet并非一种真正意义上并行机制,它只是在同一个线程里面,在不一样的代码块之间进行切换,正如单核cpu在不一样的进程之间切换同样,这个执行一下子、那个执行一下子,只不过greenlet在切换的时候须要显示指定切换到哪儿。编辑器

父greenlet

Let’s see where execution goes when a greenlet dies. Every greenlet has a “parent” greenlet. The parent greenlet is initially the one in which the greenlet was created (this can be changed at any time). The parent is where execution continues when a greenlet dies. This way, greenlets are organized in a tree. Top-level code that doesn’t run in a user-created greenlet runs in the implicit “main” greenlet, which is the root of the tree.函数

In the above example, both gr1 and gr2 have the main greenlet as a parent. Whenever one of them dies, the execution comes back to “main”.oop

Uncaught exceptions are propagated into the parent, too. For example, if the above test2() contained a typo, it would generate a NameError that would kill gr2, and the exception would go back directly into “main”. The traceback would show test2, but not test1. Remember, switches are not calls, but transfer of execution between parallel “stack containers”, and the “parent” defines which stack logically comes “below” the current one.性能

接下来看看当一个greenlet死亡以后,执行点会去哪里呢。每个greenlet都有一个父greenlet,而且都在其父greenlet中被建立(不过能够在任意时刻改变)。当子greenlet结束时,执行位置从父greenlet那里继续,这样,全部的greenlet之间就组合成了一颗树,顶级的代码不在用户建立的greenlet中运行,而是运行在一个主greenlet中,即,全部greenlet的树根

好比咱们以前的gr1和gr2都将主greenlet做为父greenlet,任何一个死掉,执行点都会回到主greenlet

未捕获的异常会传递给父greenlet,好比bar函数当中出现了异常,那么这个异常将会直接传递给主greenlet。traceback将会显示bar,而不是foo。记住:切换不是调用,而是执行点在并行的栈容器之间交换,而父greenlet定义了这些栈的前后关系。

greenlet的属性

注意的是,green是一个类,可不是一个函数。那么它的属性有哪些呢?咱们看一下源码,固然这个源码底层是C写的,可是我用的是pycharm,这个编辑器把代码抽象出来了。

class greenlet(object):

    def getcurrent(self, *args, **kwargs): # real signature unknown
        pass

    def gettrace(self, *args, **kwargs): # real signature unknown
        pass

    def settrace(self, *args, **kwargs): # real signature unknown
        pass

    def switch(self, *args, **kwargs): # real signature unknown; restored from __doc__
        pass

    def throw(self, *args, **kwargs): # real signature unknown
        pass

switch

这个咱们刚才已经看到过了,就是切换到指定的greenlet执行,并且这里面仍是能够传参的。

from greenlet import greenlet


def foo(name, age):
    print(name, age)


gr1 = greenlet(foo)
gr1.switch("satori", age=16)
"""
satori 16
"""
from greenlet import greenlet


def foo(name):
    gr2.switch(16)
    print(f"name is {name}")


def bar(age):
    print(f"age is {age}")
    gr1.switch()


gr1 = greenlet(foo)
gr2 = greenlet(bar)
gr1.switch("satori")
"""
age is 16
name is satori
"""

首先gr1.switch("satori"),会调用foo,并把参数传给name。而后调用gr2.switch(16),调用bar,并把参数传递给age而后打印,此时在bar中又调用了gr1.switch(),可是注意这里没有传入参数。其实这里传不传都无所谓,由于在初始化的时候函数栈是空的,在第一次gr1.switch的时候,已经传了,栈帧已经建立了。 bar里面的switch只不过是切换到了gr1这个greenlet当中,name仍是咱们第一次传入的"satori"。所以greenlet的switch是在不一样的greenlet之间进行切换,并非调用。

既然如此,若是我在bar里面的switch里面传入值的话,那么能不能在foo里面接收到这个值呢?显然是能够的,怎么接受,咱们能够想象一下生成器里面send

from greenlet import greenlet


def foo(name):
    x = gr2.switch(16)
    print(x)
    print(f"name is {name}")


def bar(age):
    print(f"age is {age}")
    gr1.switch("我向你传了一个参数")


gr1 = greenlet(foo)
gr2 = greenlet(bar)
gr1.switch("satori")
"""
age is 16
我向你传了一个参数
name is satori
"""

能够看到这个生成器很相似,yield xx是能够赋值的,好比value = yield xx,那么当我从这里继续前进的时候,能够经过send("aaa"),那么value就是咱们在send里面传入的aaa。同理switch也是同样,咱们也能够在bar的switch里面进行传参,这个参数就是foo里面gr1.switch函数的返回值。若是熟悉生成器和小伙伴,能够对比一下yield和send,会很容易理解。总结一下就是:函数的参数是咱们第一次switch的时候传递的,以后switch里面传递值就会做为别的switch的返回值。

咱们以前说每个greenlet都有一个parent greenlet,那么这个父greenlet是谁呢?又是在哪里建立的呢?事实上,在咱们import greenlet的时候,这个父greenlet就已经建立好了,全部的greenlet组成了一个树,这个树的根节点就是咱们尚未手动建立greenlet时候的main  greenlet。当一个协程正常结束,执行流程会回到其对应的parent;或者一个协程中出现了未被捕获的异常,该异常也是会回到其对应的parent。

from greenlet import greenlet


def foo():
    print("foo>>>", id(gr1.getcurrent()), id(gr1.getcurrent().parent))
    return "xxx"


def bar():
    print("bar>>>", id(gr2.getcurrent()), id(gr2.getcurrent().parent))


gr1 = greenlet(foo)
gr2 = greenlet(bar)
gr1.switch()
gr2.switch()
"""
foo>>> 2902341415936 2902341415776
bar>>> 2902341416096 2902341415776
"""

能够看到gr1和gr2不是同个对象,可是它们的父greenlet是同一个对象。由于默认的父greenlet取决于建立环境,同理foo函数的返回值也是返回给了父greenlet,你经过switch是获取不到的。

from greenlet import greenlet


def foo():
    return "xxx"


gr1 = greenlet(foo)
print(gr1.switch())  # xxx
# 这样是能够拿到返回值的

同理对于异常也是如此

from greenlet import greenlet


def foo():
    try:
        gr2.switch()
    except Exception:
        print("在foo中捕获了bar的异常")


def bar():
    1 / 0


gr1 = greenlet(foo)
gr2 = greenlet(bar)

try:
    gr1.switch()
except Exception:
    print("在main中捕获了bar的异常")
"""
在main中捕获了bar的异常
"""

咱们看到当从foo切换到bar的时候,bar里面出现了异常,可是这个异常并无抛给foo里面,而是抛给外面。不过可能有人好奇,我怎么没有看到父greenlet在哪儿。刚才说了,在咱们import greenlet的时候就已经建立了,咱们在外层开始gr1.switch的时候,是须要借助父greenlet的力量的,所以出现了异常也要在外层的gr1.switch中捕获。

可是,我要说可是了。其实父greenlet是能够指定的,若是我把上面的代码改一下。

from greenlet import greenlet


def foo():
    try:
        gr2.switch()
    except Exception:
        print("在foo中捕获了bar的异常")

    raise RuntimeError("出错啦")


def bar():
    1 / 0


gr1 = greenlet(foo)
gr2 = greenlet(bar, gr1)
"""
def __init__(self, run=None, parent=None):
建立greenlet的时候,是能够手动指定父parent的,若是不指定那么是main。
可是无论怎么样,总会有一个greenlet是main greenlet建立的
"""

try:
    gr1.switch()
except RuntimeError as e:
    print(e)
"""
在foo中捕获了bar的异常
出错啦
"""

咱们看到在根据bar建立greenlet的时候,咱们指定了父greenlet为gr1,那么bar中抛的异常就会在gr1中捕获,至于gr1中抛出的异常,因为它的父greenlet是main,那么就只能在最外层捕获了。

greenlet的生命周期

from greenlet import greenlet


def foo():
    gr2.switch()


def bar():
    gr1.switch()


gr1 = greenlet(foo)
gr2 = greenlet(bar)

gr1.switch()
print(f"是否死亡:{gr1.dead} {gr2.dead}")  # 是否死亡:True False
gr2.switch()
print(f"是否死亡:{gr1.dead} {gr2.dead}")  # 是否死亡:True True

咱们注意到:gr1.switch()以后,gr1已死亡,gr2未死亡。而后再gr2.switch()以后,两个greenlet都死亡了。这是为何,咱们来分析一下。有人以为,在第一次gr1.switch的时候,两个函数都应该执行完了啊,是这样吗?那return呢?因此对于一个greenlet来讲,若是切换出去了,那么必需要切换回来,不然这个greenlet的状态是不会死亡的。当咱们第二次在gr2.switch的时候,此时gr2才算是执行完毕。

那抛异常呢?

from greenlet import greenlet


def bar():
    raise


gr2 = greenlet(bar)

try:
    gr2.switch()
except Exception:
    pass

print(gr2.dead)  # True

能够看到抛了异常,也算是执行完毕了。由于不管是return仍是抛异常,函数的栈都会被清空。

若是对已经状态为dead的greenlet进行switch的话,会怎么样呢?

from greenlet import greenlet


def foo():
    print(123)


def bar():
    raise


gr1 = greenlet(foo)
gr1.switch()
print(gr1.dead)
gr1.switch()
"""
123
True
"""

能够看到没有任何反应,可是实际上会切换到其父greenlet

监控greenlet

greenlet提供了接口,可让咱们使用gettrace和settrace监控整个流程。

from greenlet import greenlet, getcurrent, settrace


def cb(event, args):
    print(f"event is {event}, args is {args}, id(args[0] is {id(args[0])}, id(args[1] is {id(args[1])})")


def foo():
    gr2.switch()


def bar():
    raise


main = getcurrent()  # main greenlet直接from greenlet import getcurrent便可
gr1 = greenlet(foo)
gr2 = greenlet(bar)

print(id(main), id(gr1), id(gr2))

# 设置回调
oldtrace = settrace(cb)

try:
    gr1.switch()
except Exception:
    print("error occurred")
finally:
    settrace(oldtrace)
"""
2882796614496 2882796614656 2882796614816
event is switch, args is (<greenlet.greenlet object at 0x0000029F34117360>, <greenlet.greenlet object at 0x0000029F34117400>), id(args[0] is 2882796614496, id(args[1] is 2882796614656)
event is switch, args is (<greenlet.greenlet object at 0x0000029F34117400>, <greenlet.greenlet object at 0x0000029F341174A0>), id(args[0] is 2882796614656, id(args[1] is 2882796614816)
event is throw, args is (<greenlet.greenlet object at 0x0000029F341174A0>, <greenlet.greenlet object at 0x0000029F34117360>), id(args[0] is 2882796614816, id(args[1] is 2882796614496)
error occurred
"""

咱们注意到:每当switch的时候都会执行cb回调函数,第一次switch,表示从main切换到gr1,第二次表示从gr1切换到gr2,若是出异常了,那么一样会切换,就会从当前greenlet切换到main greenlet。cb里面的event就是表示是switch仍是throw,args则是两个greenlet组成的元组,表示从哪一个greenlet切换到哪一个greenlet。

greenlet使用建议

  • greenlet建立以后,必定要结束。若是switch以后不回来了,那么很容易形成内存泄漏
  • python中每一个线程都有本身的main greenlet以及对应的sub-greenlet,不一样线程之间的greenlet是不能切换的
  • 不可存在循环引用
from greenlet import greenlet

l = []


def foo():
    gr2.switch()


def bar():
    l.extend([x for x in range(100)])
    gr1.switch()
    del l[:]


gr1 = greenlet(foo)
gr2 = greenlet(bar)
gr1.switch()

del gr1, gr2
print(len(l))  # 100

在bar中,给列表l添加了100个元素,可是切换出去以后再也没有切换回来,所以del l[:]这行语句就没法执行,容易形成内存泄漏。可是greenlet有一个机制,那就是若是greenlet实例的引用计数为0,那么上次挂起的地方就会抛出GreenletExit异常,咱们能够在异常中进行资源处理。但是咱们没有看到这个异常啊,并且咱们已经删除了gr1和gr2了啊,由于这个异常不会抛到main greenlet,只有你捕获才会看到。

from greenlet import greenlet

l = []


def foo():
    gr2.switch()


def bar():
    l.extend([x for x in range(100)])
    try:
        gr1.switch()
    finally:
        del l[:]


gr1 = greenlet(foo)
gr2 = greenlet(bar)
gr1.switch()

del gr1, gr2
print(len(l))  # 0

gevent介绍

gevent是一个高性能网络库,底层使用了libev事件驱动框架,核心就是咱们上面的greenlet。原理就是将python线程转化为greenlet(就是咱们说的猴子补丁,将python内的线程、ssl、socket都变成了非阻塞的),那么当greenlet遇到io操做时,好比请求网站、睡眠等待,就会切换到其余的greenlet,等到io操做完成,再自动切换回来,这是gevent自动帮咱们完成的。

理解gevent首先要理解gevent的调度流程,在gevent当中有一个hub的概念,专门用于调度全部的greenlet实例,固然这个hub也是一个greenlet,只是特殊一些,咱们也把hub称之为main thread。把全部greenlet经过switch方法注册到hub上,而后hub调度执行某一个greenlet,一旦出现io阻塞,那么控制权就会从当前的执行的greenlet回到hub。hub再去驱动其余的greenlet执行。

固然若是说是greenlet实际上不许确,应该是Greenlet,这个Greenlet是gevent一个类,可是继承自greenlet.greenlet。若是继承了,必需要重写其内部的run方法,由于greenlet.switch的时候会使用到这个run方法,固然hub也继承了greenlet.greenlet。

from gevent.greenlet import Greenlet
"""
class Greenlet(greenlet):
"""
from gevent.hub import Hub
"""
class Hub(WaitOperationsGreenlet):
class WaitOperationsGreenlet(SwitchOutGreenletWithLoop):
locals()['SwitchOutGreenletWithLoop'] = _greenlet_primitives.SwitchOutGreenletWithLoop
class SwitchOutGreenletWithLoop(TrackedRawGreenlet):
class TrackedRawGreenlet(greenlet):
"""

hub是Hub的实例对象,Hub最终也是集成自greenlet

gevent基本使用

import gevent


def f1():
    for i in range(5):
        print(f"f1:{i}")
        gevent.sleep(0)


def f2():
    for i in range(5):
        print(f"f2:{i}")
        gevent.sleep(0)


# gevent.spawn实际上就是Greenlet.spawn,建立一个greenlet,并将该greenlet的switch()加入hub主循环回调。        
t1 = gevent.spawn(f1)
t2 = gevent.spawn(f2)
# joinall则是开始执行,切换到hub,调度Greenlet
gevent.joinall([t1, t2])
"""
f1:0
f2:0
f1:1
f2:1
f1:2
f2:2
f1:3
f2:3
f1:4
f2:4
"""

可是注意的是,这里是gevent.sleep,若是是time.sleep是不会切换的。咱们须要引入一个猴子补丁

import gevent
import time
from gevent import monkey
monkey.patch_all()


def f1():
    for i in range(5):
        print(f"f1:{i}")
        time.sleep(1)


def f2():
    for i in range(5):
        print(f"f2:{i}")
        time.sleep(1)


# 传参的话,直接spawn(f1, *args, **kwargs)便可
t1 = gevent.spawn(f1)
t2 = gevent.spawn(f2)
start = time.perf_counter()
gevent.joinall([t1, t2])
print(time.perf_counter() - start)  # 5.0046873000000005
# 总体只用了5s,说明是没有阻塞的

固然gevent还有其余不少用法,这里就不看了,由于关于并发,我的仍是推荐使用asyncio,只是由于celery,因此须要研究一下eventlet,从而须要研究gevent、greenlet。

相关文章
相关标签/搜索