用greenlet实现Python中的并发
在上一篇介绍生成器时,我们讲到了协程(Coroutine),它也被称为微线程。回顾一下,协程可以在一个函数执行过程中将其挂起,去执行另一个函数,并在必要时将之前的函数唤醒。在Python的语言环境里,协程是相当常用的实现”并发”的方法。上一篇的例子中,我们演示了如何使用yield关键字来实现协程,不过这个看上去非常不直观。这里我们要介绍一个非常好用的框架greenlet,很多知名的网络并发框架如eventlet,gevent都是基于它实现的。
第一个例子
沿袭我们一直以来的习惯,先从例子开始,这次偷个懒,直接把官方文档中的例子拿过来:
from greenlet import greenlet
def test1():
print 12
gr2.switch()
print 34
def test2():
print 56
gr1.switch()
print 78
gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch()
这里创建了两个greenlet协程对象,gr1
和gr2
,分别对应于函数test1()
和test2()
。使用greenlet对象的switch()
方法,即可以切换协程。上例中,我们先调用gr1.switch()
,函数test1()
被执行,然后打印出”12”;接着由于gr2.switch()
被调用,协程切换到函数test2()
,打印出”56”;之后gr1.switch()
又被调用,所以又切换到函数test1()
。但注意,由于之前test1()
已经执行到第5行,也就是gr2.switch()
,所以切换回来后会继续往下执行,也就是打印”34”;现在函数test1()
退出,同时程序退出。由于再没有gr2.switch()
来切换至函数test2()
,所以程序第11行”print 78”不会被执行。
所以,程序运行下来的输出就是:
12
56
34
很好理解吧。使用switch()
方法切换协程,也比”yield”, “next/send”组合要直观的多。上例中,我们也可以看出,greenlet协程的运行,其本质是串行的,所以它不是真正意义上的并发,因此也无法发挥CPU多核的优势,不过,这个可以通过协程+进程组合的方式来解决,本文就不展开了。另外要注意的是,在没有进行显式切换时,部分代码是无法被执行到的,比如上例中的print 78
。
父子关系
创建协程对象的方法其实有两个参数greenlet(run=None, parent=None)
。参数run
就是其要调用的方法,比如上例中的函数test1()
和test2()
;参数parent
定义了该协程对象的父协程,也就是说,greenlet协程之间是可以有父子关系的。如果不设或设为空,则其父协程就是程序默认的”main”主协程。这个”main”协程不需要用户创建,它所对应的方法就是主程序,而所有用户创建的协程都是其子孙。大家可以把greenlet协程集看作一颗树,树的根节点就是”main”,上例中的gr1
和gr2
就是其两个字节点。
在子协程执行完毕后,会自动返回父协程。比如上例中test1()
函数退出,代码会返回到主程序。让我们写个更清晰的例子来实验下:
from greenlet import greenlet
def test1():
print 12
gr2.switch()
print 34
def test2():
print 56
gr1 = greenlet(test1)
gr2 = greenlet(test2, gr1)
gr1.switch()
print 78
这里创建greenlet对象gr2
时,指定了其父协程是gr1
。所以在函数test2()
里,虽然没有gr1.switch()
代码,但是在其退出后,程序一样回到了函数test1()
,并且执行print 34
。同样,在test1()
退出后,代码回到了主程序,并执行print 78
。所以,最后的输出就是:
12
56
34
78
如果上例中,gr2
的父协程不是gr1
而是”main”的话,那test2()
运行完毕就会回到主程序并直接打印”78”,这样print 34
就不会执行。大家可以试一试。
还有一个重要的点,就是协程退出后,就无法再被执行了。如果上例在函数test1()
中,再加一句gr2.switch()
,运行的结果是一样的。因为第二次调用gr2.switch()
,什么也不会运行。
def test1():
print 12
gr2.switch()
print 34
gr2.switch()
大家可能会感觉到父子协程之间的关系,就像函数调用一样,一个嵌套一个。的确,其实greenlet协程的实现就是使用了栈,其运行的上下文保存在栈中,”main”主协程处于栈底的位置,而当前运行中的协程就在栈顶。这同函数是一样。此外,在任何时候,你都可以使用greenlet.getcurrent()
,获取当前运行中的协程对象。比如在函数test2()
中执行greenlet.getcurrent()
,其返回就等于gr2
。
异常
既然协程是存放在栈中,那一个协程要抛出异常,就会先抛到其父协程中,如果所有父协程都不捕获此异常,程序才会退出。我们试下,把上面的例子中函数test2()
的代码改为:
def test2():
print 56
raise NameError
程序执行后,我们可以看到Traceback信息:
File "parent.py", line 14, in <module>
gr1.switch()
File "parent.py", line 5, in test1
gr2.switch()
File "parent.py", line 10, in test2
raise NameError
同时大家可以试下,如果将gr2
的父协程设为空,Traceback信息就会变为:
File "parent.py", line 14, in <module>
gr1.switch()
File "parent.py", line 10, in test2
raise NameError
因此,如果gr2
的父协程是gr1
的话,异常先回抛到函数test1()
的代码gr2.switch()
处。所以,我们再对函数test1()
改动下:
def test1():
print 12
try:
gr2.switch()
except NameError:
print 90
print 34
运行后的结果,如果gr2
的父协程是gr1
,则异常被捕获,并打印”90”。否则,异常会被抛出。以上实验很好的证明了,子协程抛出的异常会根据栈里的顺序,依次抛到父协程里。
有一个异常是特例,不会被抛到父协程中,那就是greenlet.GreenletExit
,这个异常会让当前协程强制退出。比如,我们将函数test2()
改为:
def test2():
print 56
raise greenlet.GreenletExit
print 78
那代码行print 78
永远不会被执行。但这个异常不会往上抛,所以其父协程还是可以正常运行。
另外,我们可以通过greenlet对象的throw()
方法,手动往一个协程里抛个异常。比如,我们在test1()
里调一个throw()
方法:
def test1():
print 12
gr2.throw(NameError)
try:
gr2.switch()
except NameError:
print 90
print 34
这样,异常就会被抛出,运行后的Trackback是这样的:
File "exception.py", line 21, in <module>
gr1.switch()
File "exception.py", line 5, in test1
gr2.throw(NameError)
如果将gr2.throw(NameError)
放在”try”语句中,那该异常就会被捕获,并打印”90”。另外,当gr2
的父协程不是gr1
而是”main”时,异常会直接抛到主程序中,此时函数test1()
中的”try”语句就不起作用了。
协程间传递消息
在介绍生成器时,我们聊过可以使用生成器的send()
方法来传递参数。greenlet也同样支持,只要在其switch()
方法调用时,传入参数即可。我们再来基于本文第一个例子改造下:
from greenlet import greenlet
def test1():
print 12
y = gr2.switch(56)
print y
def test2(x):
print x
gr1.switch(34)
print 78
gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch()
在test1()
中调用gr2.switch()
,由于协程gr2
之前未被启动,所以传入的参数”56”会被赋在test2(
)函数的参数x
上;在test2()
中调用gr1.switch()
,由于协程gr1
之前已执行到第5行y = gr2.switch(56)
这里,所以传入的参数”34”会作为gr2.switch(56)
的返回值,赋给变量y
。这样,两个协程之间的互传消息就实现了。
让我们将上一篇介绍生成器时写的生产者消费者的例子,改为greenlet实现吧:
from greenlet import greenlet
def consumer():
last = ''
while True:
receival = pro.switch(last)
if receival is not None:
print 'Consume %s' % receival
last = receival
def producer(n):
con.switch()
x = 0
while x < n:
x += 1
print 'Produce %s' % x
last = con.switch(x)
pro = greenlet(producer)
con = greenlet(consumer)
pro.switch(5)
更多参考资料
本文中的示例代码可以在这里下载。