Greenlet: 轻量级并发编程

written on Wed 18 May 2016 by

动机

greenlet这个包拆分自Stackless。Stackless是一个CPython的版本,实现并支持一种叫做tasklets的微线程。Tasklets会以一种伪并发的方式运行(通常运行在单个或者一些系统级的线程中),它们之间通过channels来同步数据。

一个greenlet,从另一方面来说,依然是一种很原始的没有隐式调度的微线程。换句话说,就是协程(coroutine)。这在你想要完全控制代码的运行时是非常有用的。你可以在greenlet之上构建采用自定义调度方式的微线程。然而,使用greenlet来制作先进的控制流结构是很有用的。举个例子,我们可以重新创造生成器。和Python自带的生成器所不同的是,我们的生成器可以调用网状的方法,而且这些网状的方法也可以yield出值。(另外,你不需要再使用yield这个关键词了)

举例

我们来思考一个程序,用户可以在一个类终端控制台中输入命令来控制该程序。假设命令是一个字符一个字符的输入。在这样一个系统中,通常会存在如下一个循环:

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    # ignore the command
        process_command(line)

如果现在假设你想要将这个程序插入到用户界面中。大部分的用户界面工具都是基于事件驱动的,它们会在用户输入一个字符后调用回调函数。在这种设定下,编写上述代码所需的read_next_char()函数是非常难的,将会有如下两个冲突的函数:

def event_keyworn(key):
    ??

def read_next_char():
    ??应该等待下一个event_keydown()函数的调用

显然,上述情况在串行运行是不可能的。你也许想到了使用多个线程来完成。使用Greenlets作为替代方法,可以免去关联锁和程序退出的相关问题。你可以在程序运行主干中分裂出一个greenlet来专门运行process_commands()函数,你可以使用如下方式来交换用户的按键情况:

def event_keydown(key):
    # 跳转至g_processor, 并且把用户所按键发送给g_processor
    g_processor.switch(key)

def process_commands():
    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    # ignore the command
        process_command(line)

def read_next_char():
    # 在这个例子里,g_self就是g_processor
    g_self = greenlet.getcurrent()
    # 跳转到父greenlet(主greenlet)中,等待下一个按键
    next_char = g_self.parent.switch()
    return next_char

g_processor = greenlet(process_commands)
g_processor.switch(*args)   # input arguments to process_commands()

gui.mainloop()

在这个例子中,执行过程如下: 当read_next_char()被调用时,身处于g_processor这个greenlet中,所以它的父greenlet也就是派生g_processor的根greenlet。当它显示切换回父greenlet的时候,程序会重新回到顶层继续GUI事件监听循环。当事件监听循环回调event_keydown()函数的时候, 又切换回g_processor,这意味着程序会跳转到目标greenlet之前被挂起的地方继续执行-在这个例子里,将会跳转会read_next_char()函数中调用switch()的地方继续执行,并且在event_keydown()中调用switch()时所给的参数key会被做为返回值,并赋值给变量next_char。

注意,read_next_char()函数在被挂起和恢复时,它的调用栈会得到保护。所以它会在process_commands()中的不同位置返回,这主要取决于它原先被调用的位置。这使程序的执行逻辑以一种很好的控制流得以保持。我们不需要完全重写process_commands()来使其转化为状态机。

使用

简介

一个greenlet其实是一个独立的微伪线程。把它想象成一个小型的frame栈。最外层的frame就是你调用的初始函数,最里层的frame就是greenlet目前正停留的frame。在你使用greenlets的时候,就是通过创建大量的这种栈,并且在它们之间跳跃执行。跳跃(切换)永远都是显式的。一个greenlet必须显式切换至目标greenlet,这样会使前者的执行被挂起,而目标greenlet会在之前挂起的地方被恢复过来继续执行。greenlets之间的跳跃被称为切换

当你创建了一个greenlet,它会的到一个初始的空栈;当第一次切换到这个greenlet的时候,它开始执行一个指定的函数,在这个函数中又可能会调用其他函数,切换至其他greenlet。最终当最外层的函数执行完毕后,整个greenlet的调用栈再次变空,那么这个greenlet就死了。greenlet也可能死于未被捕获的异常。

举个例子:

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所在的greenlet执行完成并且死亡。此时,程序回到最开始的gr1.switch()继续执行。注意,78将永远不会被执行。

父级

让我们看看当某个greenlet死亡的的时候,程序会如何执行。每一个greenlet都会有一个父greenlet。顾名思义,父greenlet就是分裂出子greenlet的greenlet(不过可以随时通过greenlet.parent来修改某一个greenlet的父greenlet)。当greenlet结束的时候,程序会回到他的父greenlet继续执行。这样,所有的greenlet形成树结构。树的根节点其实是隐式的主greenlet,所以不在用户自定义的greenlet中执行的代码都会在主greenlet中被执行。

在上面的例子中,gr1和gr2的父greenlet都是最外层的主greenlet,不论是它们中谁结束了,程序就会回到主greenlet继续执行。

greenlet未被捕获的异常也会往外抛给父级greenlet。举个例子,如果上面例子中的函数test2包含一个拼写错误,那么所产生的NameError异常会干死gr2,程序便会直接回到主greenlet执行。而traceback会包含test2,但不会包含test1。记住,switch不是调用,而是在多个并行的栈容器之间切换执行。父级表示了它的栈在逻辑上是在当前greenlet的下方的。

实例

greenlet.greenlet是greenlet类型, 它支持以下操作:

greenlet(run=None, parent=None) 创建新的greenlet对象(不会运行)。run参数执行需要执行的函数,parent指定它的父级greenlet,默任的话就是当前的greenlet。

greenlet.getcurrent() 返回当前所在的greenlet(即调用该函数的greenlet)

greenlet.GreenletExit 这个特定的异常并不会被往外抛给其父级greenlet;使用它可以杀死一个greenlet。

greenlet类也可以被继承。greenlet的run属性一般是在greenlet创建时被设置,调用run可以启动该greenlet。但当你定义greenlet的子类时,重写其run方法比在构造函数中传入run参数来的有意义。

切换

greenlet之间的切换发生在某个greenlet的switch函数被调用的时候,亦或是某个greenlet结束的时候(程序会返回父级greenlet继续执行)。在切换期间,一个对象或异常会被发送给目标greenlet。这可以作为一种方便的方法在greenlet之间传递信息。比如:

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的参数不是在greenlet被创建时所提供,而是在第一次切换到它们执行时提供。

g.switch(*args, **kwargs) 将执行权切换给greenlet g, 并将指定参数发送给它。特别的是,如果g还没有启动,那么此时g将会启动。

greenlet之死 如果greenlet的run函数执行完了,函数的返回值将会被发送给其父级。如果run函数被异常所终止,则异常也会被抛给其父级(除非是greenlet.GreenletExit异常,该异常会被捕获,函数执行结束并返回父级)。

除了上述之外,目标greenlet通常接收对象作为调用之前已被挂起的greenlet的switch()函数的返回值。实际上,尽管对于switch()函数的调用不会立即返回,但是仍然会在未来的某个时刻返回(可能是别的greenlet切换时给出的参数,也可能是之前switch的greenlet的函数返回值)。此时,程序会回到之前被挂起的位于对于switch()函数的调用处。这表示 x = g.switch(y) 会把y发送给greenlet g,然后过段时间之后,会接收到一个对象并赋值给x。

注意,对于任何已死greenlet的切换操作都会走到它们的父辈,或者父父辈,以此类推。最后的父级greenlet就是整棵树的根节点maingreenlet,因为它永远不会死亡。

greenlet的属性和方法

g.switch(*args, **kwargs) 切换至greenlet g

g.run greenlet启动时将会执行的函数。当greenlet g 启动后该属性将不复存在。

g.parent 父级greenlet。该属性可修改,但是不允许形成循环父子结构。

g.gr_frame The current top frame, or None. 当前顶层frame,没有则是None。

g.dead 如果greenlet g已经执行结束了,则返回True。

bool(g) 如果greenlet g还活着,则返回True,结束活着还未开始都是False。

g.throw([typ, [val, [tb]]]) 切换至greenlet g,不过会立刻在g中抛出给定的异常。如果没有指定任何参数,那么默认抛出greenlet.GreenletExit,那么g就结束了。调用这个函数就相当于下面的过程:

def raiser():
    raise typ, val, tb
g_raiser = greenlet(raiser, parent=g)
g_raiser.switch()

注意,上述代码对于greenlet.GreenletExit无效,因为该异常不会被抛给父级greenlet g

greenlets和Python线程

greenlet可以和Python的线程结合使用;这种情况下,每一个线程将包含一个主greenlet和大量子greenlet,形成树形结构。对于属于不同线程的greenlets,混合或者切换操作是不可能的。

greenlet的垃圾回收

如果对于一个greenlet对象的所有引用都不存在了(包括来自其他greenlet的parent属性的引用),然后就没有办法再切换到这个greenlet了。这种情况下,GreenletExit异常就会在该greenlet中产生,这是一个greenlet唯一的一种异步获取执行权的情况。你可以使用try:finally:语句块来清理该greenlet所占有的资源。这个特性同时也支持那种使用死循环接收并处理数据的编码风格。当对于greenlet的最后一个引用被干掉后,死循环就会自动终止了。

通过在某处保存对于某个greenlet的新引用可以认为这个greenlet可以正常死亡或重新恢复。只要捕获并忽略GreenletExit异常就可以让这个greenlet进入死循环。

Greenlet不参与垃圾回收;greenlet中的循环引用将不会被发现,循环保存对于greenlet的引用可能会导致内存泄露。

调用跟踪支持

标准的Python调用跟踪和性能分析在greenlet中不能正常工作,这是因为栈和frame的切换发生在同一个线程之中。使用传统的方法来发现可靠的greenlet切换操作是很困难的,所以greenlet模块为基于greenlet的代码提供了新的调试,追踪和分析功能:

greenlet.gettrace() 返回先前设定的调用跟踪函数,如果没有则返回None。

greenlet.settrace(callback) 设定新的调用跟踪函数并返回之前设定的调用跟踪函数,如果没有则返回None。回调函数会被不同的事件所调用,并且需要行如:

def callback(event, args):
    if event == 'switch':
        origin, target = args
        # Handle a switch from origin to target.
        # Note that callback is running in the context of target
        # greenlet and any exceptions will be passed as if
        # target.throw() was used instead of a switch.
        return
    if event == 'throw':
        origin, target = args
        # Handle a throw from origin to target.
        # Note that callback is running in the context of target
        # greenlet and any exceptions will replace the original, as
        # if target.throw() was used with the replacing exception.
        return

如果事件类型既有'switch'又有'throw', 那么对于参数元组args的解包就非常重要。这样的话,API就可以像sys.settrace()那样被扩展为更多的事件类型。

两个官方给出的例子

1. 简单的生成器
import unittest
from greenlet import greenlet


class genlet(greenlet):

    def __init__(self, *args, **kwds):
        self.args = args
        self.kwds = kwds

    def run(self):
        fn, = self.fn
        fn(*self.args, **self.kwds)

    def __iter__(self):
        return self

    def __next__(self):
        self.parent = greenlet.getcurrent()
        result = self.switch()
        if self:
            return result
        else:
            raise StopIteration

    # Hack: Python < 2.6 compatibility
    next = __next__


def Yield(value):
    g = greenlet.getcurrent()
    while not isinstance(g, genlet):
        if g is None:
            raise RuntimeError('yield outside a genlet')
        g = g.parent
    g.parent.switch(value)


def generator(func):
    class generator(genlet):
        fn = (func,)
    return generator


class GeneratorTests(unittest.TestCase):
    def test_generator(self):
        seen = []

        def g(n):
            for i in range(n):
                seen.append(i)
                Yield(i)
        g = generator(g)
        for k in range(3):
            for j in g(5):
                seen.append(j)
        self.assertEqual(seen, 3 * [0, 0, 1, 1, 2, 2, 3, 3, 4, 4])
2. 网状调用生成器
import unittest
from greenlet import greenlet


class genlet(greenlet):

    def __init__(self, *args, **kwds):
        self.args = args
        self.kwds = kwds
        self.child = None

    def run(self):
        fn, = self.fn
        fn(*self.args, **self.kwds)

    def __iter__(self):
        return self

    def set_child(self, child):
        self.child = child

    def __next__(self):
        if self.child:
            child = self.child
            while child.child:
                tmp = child
                child = child.child
                tmp.child = None

            result = child.switch()
        else:
            self.parent = greenlet.getcurrent()
            result = self.switch()

        if self:
            return result
        else:
            raise StopIteration

    # Hack: Python < 2.6 compatibility
    next = __next__


def Yield(value, level=1):
    g = greenlet.getcurrent()

    while level != 0:
        if not isinstance(g, genlet):
            raise RuntimeError('yield outside a genlet')
        if level > 1:
            g.parent.set_child(g)
        g = g.parent
        level -= 1

    g.switch(value)


def Genlet(func):
    class Genlet(genlet):
        fn = (func,)
    return Genlet


def g1(n, seen):
    for i in range(n):
        seen.append(i + 1)
        yield i


def g2(n, seen):
    for i in range(n):
        seen.append(i + 1)
        Yield(i)

g2 = Genlet(g2)


def nested(i):
    Yield(i)


def g3(n, seen):
    for i in range(n):
        seen.append(i + 1)
        nested(i)
g3 = Genlet(g3)


def a(n):
    if n == 0:
        return
    for ii in ax(n - 1):
        Yield(ii)
    Yield(n)
ax = Genlet(a)


def perms(l):
    if len(l) > 1:
        for e in l:
            # No syntactical sugar for generator expressions
            [Yield([e] + p) for p in perms([x for x in l if x != e])]
    else:
        Yield(l)
perms = Genlet(perms)


def gr1(n):
    for ii in range(1, n):
        Yield(ii)
        Yield(ii * ii, 2)

gr1 = Genlet(gr1)


def gr2(n, seen):
    for ii in gr1(n):
        seen.append(ii)

gr2 = Genlet(gr2)


class NestedGeneratorTests(unittest.TestCase):
    def test_layered_genlets(self):
        seen = []
        for ii in gr2(5, seen):
            seen.append(ii)
        self.assertEqual(seen, [1, 1, 2, 4, 3, 9, 4, 16])

    def test_permutations(self):
        gen_perms = perms(list(range(4)))
        permutations = list(gen_perms)
        self.assertEqual(len(permutations), 4 * 3 * 2 * 1)
        self.assertTrue([0, 1, 2, 3] in permutations)
        self.assertTrue([3, 2, 1, 0] in permutations)
        res = []
        for ii in zip(perms(list(range(4))), perms(list(range(3)))):
            res.append(ii)
        self.assertEqual(
            res,
            [([0, 1, 2, 3], [0, 1, 2]), ([0, 1, 3, 2], [0, 2, 1]),
             ([0, 2, 1, 3], [1, 0, 2]), ([0, 2, 3, 1], [1, 2, 0]),
             ([0, 3, 1, 2], [2, 0, 1]), ([0, 3, 2, 1], [2, 1, 0])])
        # XXX Test to make sure we are working as a generator expression

    def test_genlet_simple(self):
        for g in [g1, g2, g3]:
            seen = []
            for k in range(3):
                for j in g(5, seen):
                    seen.append(j)
            self.assertEqual(seen, 3 * [1, 0, 2, 1, 3, 2, 4, 3, 5, 4])

    def test_genlet_bad(self):
        try:
            Yield(10)
        except RuntimeError:
            pass

    def test_nested_genlets(self):
        seen = []
        for ii in ax(5):
            seen.append(ii)

结尾

了解greenlet主要是为了深入了解gevent做铺垫,翻译呢主要是为了加深自己的记忆:)

This entry was tagged on #concurrent and #gevent

comments powered by Disqus
 

Tags