Greenlet初识

翻译自官方文档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

那么下次编写并发程序的时候,是不是该考虑一下协程呢?

你可能感兴趣的:(Greenlet初识)