翻译自官方文档greenlet。
什么是greenlet
greenlet是从Stackless中分离的项目。greenlet也叫微线程、协程,它的调度是由程序明确控制的,所以执行流程是固定的、明确的。而线程的调度完全交由操作系统,执行顺序无法预料。同时,协程之间切换的代价远比线程小。
greenlet是通过C扩展实现的。
示例
有这么一个系统,它根据用户在终端输入命令的不同而执行不同的操作,假设输入是逐字符的。部分代码可能是这样的:
def process_commands(*args):
while True:
line = ''
while not line.endswith('\n'):
line += read_next_char()
if line == 'quit\n':
print "are you sure?"
if read_next_char() != 'y':
continue # 忽略当前的quit命令
process_command(line)
现在我们想把这个程序在GUI中实现。然而大多数GUI库都是事件驱动的,每当用户输入都会调用一个回调函数去处理。在这种情况下,如果还想用上面的代码逻辑,可能是这样的:
def event_keydown(key):
??
def read_next_char():
?? # 必须等待下一个event_keydown调用
read_next_char
要阻塞等待event_keydown
调用,然后就会和事件循环相冲突。这种需要并发的情况是可以用多线程来处理,但是我们有更好的方法,就是greenlet。
def event_keydown(key):
# 跳到g_processor,将key发送过去
g_processor.switch(key)
def read_next_char():
# 在这个例子中,g_self就是g_processor
g_self = greenlet.getcurrent()
# 跳到父greenlet,等待下一个Key
next_char = g_self.parent.switch()
return next_char
g_processor = greenlet(process_commands)
g_processor.switch(*args)
gui.mainloop()
我们先用process_commands
创建一个协程,然后调用switch切换到process_commands
中去执行,并输入参数args。在process_commands
中运行到read_next_char
,又切换到主协程,执行gui.mainloop()
,在事件循环中等待键盘按下的动作。当按下某个键之后,调用event_keydown
,切换到g_processor
,并将key传过去。read_next_char
恢复运行,接收到key,然后返回给process_commands
,处理完之后又暂停在read_next_char
等待下一次按键。
下面我们来详细讲解greenlet的用法。
用法
简介
一个协程是一个独立的“假线程”。可以把它想成一个小的帧栈,栈底是你调用的初始函数,栈顶是greenlet当前暂停的地方。我们使用协程,实际上就是创建了一系列这样帧栈,然后在它们之间跳转执行。而跳转必须是明确的,跳转也称为'switching'。
当你创建一个协程时,产生一个空的栈,在第一次切换到这个协程时,它调用一个特殊的函数,这个函数中可以调用其他函数,可以切换到其他协程等等。当最终栈底函数执行完后,协程的栈变为空,这时候,协程是死的(dead)。协程也可能由于异常而死亡。
下面是个非常简单的例子:
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()
最后一行切换到test1,打印12,切换到test2,打印56,又切回到test1打印34。然后test1结束,gr1死亡。这时候执行回到了gr1.switch()
调用。注意到,78并没有被打印出。
父协程
每个协程都有一个父协程。协程在哪个协程中被创建,那么这个协程就是父协程,当然后面可以更改。当某个协程死亡后,会在父协程中继续执行。举个例子,在g1中创建了g2,那么g1就是g2的父协程,g2死亡后,会在g1中继续执行。这么说的话,协程是树结构的。最上层的代码不是运行在用户定义的协程中,而是在一个隐式的主协程中,它是协程树的根(root)。
在上面的例子中,gr1和gr2的父协程都是主协程。不管哪一个死亡,执行都会回到主协程。
异常也会被传到父协程。比如说,test2中若包含了一个'typo',就会引发NameError异常,然后杀死gr2,执行会直接回到主协程。Traceback会显示test2而不是test1。注意,协程的切换不是调用,而是在平行的"栈容器"中传递执行。
协程类
greenlet.greenlet就是协程类,它支持下面一些操作:
greenlet(run=None, parent=None)
创建一个新的协程对象。run是一个可调用对象,parent是父协程,默认是当前协程。
greenlet.getcurrent()
返回当前协程,也就是调用这个函数的协程。
greenlet.GreenletExit
这个特殊的异常不会传给父协程,常用来杀死协程。
greenlet是可以被继承的。协程通过执行run属性来运行。在子类中,可以自由地去定义run,而不是一定要传递run参数给构造器。
切换
有两种情况会发生协程之间的切换。一是某个协程主动调用switch方法,这种情况下会切换到被调用的协程中。二是协程死亡,这时协程会切换到父协程。在切换时,一个对象或异常被传递到目标协程。这用来在协程之间传递信息。如下面这个例子:
def test1(x, y):
z = gr2.switch(x+y)
print z
def test2(u):
print u
gr1.switch(42)
gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch("hello", " world")
这个代码会打印"hello world"和42。注意到,test1和test2在协程创建时并没有提供参数,而是在第一次切换的地方。
g.switch(*args, **kwargs)
切换到协程g执行,传递提供的参数。如果g还没运行,那么传递参数给g的run属性,并开始执行run()。
如果协程的run()执行结束,return的值会返回给主协程。如果run()以异常方式结束,异常会传递给主协程(除非是greenlet.GreenletExit
,这种情况下会直接返回到主协程)。
如果切换到一个已死亡的的协程,那么实际上是切换到它的父协程,依次类推。
协程的方法和属性
g.switch(*args, **kwargs)
切换到协程g执行,见上面。
g.run
一个可调用对象,当g开始执行时,调用它。但是一旦开始执行后,这个属性就不存在了。
g.parent
父协程,这个值是可以改变的,但是不允许创建循环的父进程。
g.gr_frame
当前最顶层的帧,或者是None。
g.dead
如果协程已死亡,那么值是True。
bool(g)
如果协程处于活跃状态,则为True。如果已死亡或者未开始执行则为False。
g.throw([typ, [val, [tb]]])
切换到g执行,但是立刻引发异常。如果没有参数,则默认引发greenlet.GreenletExit异常。这个方法的执行类似于:
def raiser():
raise typ, val, tb
g_raiser = greenlet(raiser, parent=g)
g_raiser.switch()
当然greenlet.GreenletExit除外。
协程和Python线程
协程可以和线程组合使用。每个线程包含一个独立的主协程和协程树。当然不同线程的协程之间是无法切换执行的。
垃圾收集
如果对一个协程的引用计数为0,那么就没办法再次切换到这个协程。这种情况下,协程会产生一个GreenletExit异常。这是协程唯一一种异步接收到GreenletExit异常的情况。可以用try...finally...来清除协程的资源。这个特性允许我们用无限循环的方式来等待数据并处理,因为当协程的引用计数变成0时,循环会自动中断。
在无限循环中,如果想要协程死亡就捕获GreenletExit异常。如果想拥有一个新的引用就忽略GreenletExit。
greenlet不参与垃圾收集,目前协程帧的循环引用数据不会被检测到。循环地将引用存到其他协程会导致内存泄漏。
追踪支持
当我们使用协程的时候,标准的Python追踪和性能分析无能为力,因为协程的切换时在单个线程中。很难通过简单的方法来侦测到协程的切换,所以为了提高对调试的支持,增加了下面几个新的函数:
greenlet.gettrace()
返回之前的追踪函数设置,或者None。
greenlet.settrace(callback)
设置一个新的追踪函数,返回之前的,或者None。这个函数类似于sys.settrace()
各种事件发生的时候都会调用callback,并且callback是下面这样的:
def callback(event, args):
if event == 'switch':
origin, target = args
# 处理从origin到target的切换
# 注意callback在target的上下文中执行
return
if event == 'throw':
origin, target = args
# 处理从origin到target的抛出
# 注意callback在target的上下文中执行
return
那么下次编写并发程序的时候,是不是该考虑一下协程呢?