gevent,greenlet一些研究

1,062 阅读9分钟

最近在看gevent,起手第一句就说:

gevent is a coroutine -based Python networking library that uses greenlet to provide a high-level synchronous API on top of the libev or libuv event loop.

既然提及到了greenlet,看了一下文档并没有多少,就过去看看。

greenlet

“greenlet”是一个更原始的微线程概念,没有隐式调度。

协程,在你打算非常精确控制你的程序运行的时候,是个非常有用的东西。python利用yield构建的生成器来实现协程我在之前有说过了。而在这之上,我们可以构建一个新的生成器(generators)。不同于python自己的生成器的是,我们的生成器可以调用嵌套的函数并且嵌套的函数也可以yield出value。(并且,你并不需要yield这个关键字。后面有例子说明)

Greenlets是作为一般未修改过的python解释器的c扩展模块。

Example

让我们来考虑一种情况:当用户要输入命令,系统被终端控制台所接管的时候。假设用户是一个一个字母输入的,那么这样一个系统的话,这里有个典型的loop循环程序:

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)

这看上去没什么问题。那么当我们把这段程序嵌入一个GUI的时候,大部分GUI toolkits是事件基础的。意味着他们会在用户每按下一个字母的时候调用回调。在这个设定下,很难去实施这段代码需要的read_next_char()函数。我们有以下两个不兼容的方法:

def event_keydown(key):
    ??

def read_next_char():
    ?? should wait for the next event_keydown() call

你也许会考虑到使用线程去实施,而Greenlets是个另外一种不需要锁并且可以解决这个问题的方法。首先从process_commands() 方法里面开始,分出greenlet,然后替换keypresses:

def event_keydown(key):
         # jump into g_processor, sending it the key
    g_processor.switch(key)

def read_next_char():
        # g_self is g_processor in this simple example
    g_self = greenlet.getcurrent()
        # jump to the parent (main) greenlet, waiting for the next key
    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时候,最高级别的主循环(the GUI)开始恢复执行。因此每次当GUI调用event_keydown()时候,它切换到g_processor。意味着在greenlet中,无论事件在哪里暂停的,都会跳回来继续执行——而在这个案例,指的是read_next_char()这个方法的switch()这条指令——同时在read_next_char()这个函数,event_keydown()这个方法会将key参数作为switch()方法的返回值。

值得注意的是read_next_char() 将会被暂停,恢复的时候他的调用堆栈(call stack)将会被保留。因此到底它会返回到process_commands() 的那个位置,取决于他开始的时候在哪里被调用。这样的话,也允许程序的逻辑将会保持一个比较良好的控制流。我们不必完全重写process_commands()将其转换为状态机(state machine)。

使用

介绍

当你新建一个greenlet时候,他生成一个空的栈。当你第一次switch到它的时候,它开始运行一个特定的function,而这个function也许会调起其他functions,switch到其他greenlet等等。到了最后,最外层的function完成了它的执行流程,greenlet栈将会再次变空且greenlet处于"dead"状态。当然了,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结束并且gr1死亡。与此同时,执行返回到初始gr1.switch()调用的地方。值得注意的是,78是从头到尾都不会 被输出的。

Parents

当一个greenlet死亡的时候,执行将会如何进行呢?每一个greenlet都有一个“parent”greenlet,这个parent greenlet当greenlet创建时候,被自动初始化的一个(随时可以被更改)。而当这个greenlet死亡的时候,parent将会继续执行。这也意味这,greenlets会被以"树"的形式所组织。而最高等级的代码,并不运行在用户创建的greenlet,而是隐式的运行在“main” greenlet,同时这也是树的根节点。

上面的例子中,gr1和gr2都把main greenlet作为parent。无论时候其中它们一个死亡,执行将会返回main

而未捕获的异常同时也会向上传递给parent,比如上面的test2()如果里面发送了异常,异常将会向上传递给main而不是test1.

Instantiation

greenlet(run=None, parent=None)

新建一个greenlet(不运行),传递进run应该是一个callable,同时parent指的是parent greenlet,默认是当前greenlet。

greenlet.GreenletExit

这个特殊的异常 并不会传递回它的parent gevent。同时是用来结束一个单独的greenlet。

Switching

当一个greenlet执行它的switch()方法时候,greenlets之间的switch将会发生,执行将会跳到那个调用switch方法的greenlet里面。还有一种情况greenlets之间的switch也会发生,就是greenlet死亡,执行返回parent时候。在switch时候,一个异常目标将会被传递到目标greenlet,这也是一种greenlets传递信息的方便方法。

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所需要的参数在create时候并不需要,只有当其他greenletswitch到它的时候才需要。

g.switch(*args, **kwargs)

切换执行到greenlet g, 发送给定的参数. 如果g还没有开始执行,那就立即启动他。

did not start yet, then it will start to run now.

Dying greenlet

当一个greenlet run() 完成, 它讲return一个值给它的parent. 如果 run()是以异常终止的话, 这个异常将会返回给它的parent (除了greenlet.GreenletExit 异常, 这个异常将会被捕获和返回值给它父母)。

个人理解,这里和之前的yield from的异常处理有点像了,只捕获并处理特定的终止异常,返回终止异常的值给调用的parent。其他异常则向上冒泡。

目标greenlet会接受一个目标作为返回值,作为目标greenlet调用switch()方法,暂停函数时候的返回值。确定,虽然调用switch()并不会马上返回,但是他会在未来某个时间点,当某个greenlet切换回来时候返回。一旦这个发生,执行将会从之前switch()切换出去的地方继续执行,同时switch()方法会返回刚刚send进去的目标。这就意味着x = g.switch(y)将会发生yg,同时后面将把其他greenlet传回来的一个object作为返回值赋值给x

注意任何打算切回到dead greenlet将会去到dead greenlet's parent,或者parent的parent等等。最终的父母是maingreenlet,它不会死去。

g.throw([typ, [val, [tb]]])

传递异常到greenlet g, 将会立即在 g里面raise出相应的异常。如果不提供参数的话将会默认 是greenlet.GreenletExit。等价于

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

上面的一个例外是greenlet.GreenletExit,这并不会从g_raiser传递给g就是了。

Garbage-collecting live greenlets

按照文档的说法,greenlets并不参与python的garbage collection。循环引用并不会被检测到。

同时,如果一个greenlet的引用被清除了(包括其他parent attribute对它的引用),是没有任何办法再切回这个greenlet的。

在这种情况下,一个GreenletExit将会在greenlet内部发送。这也给了try:finally:块一个机会去清理资源。这个feature也使得greenlets可以无限地等待数据并且处理数据。当引用被清楚的时候,这个循环将会被自动中断。

greenlet期望的状态分两种:die或者被一个在其他地方安置好的引用再次复活。因此,捕获并且忽略GreenletExit这个异常将会导致一个无限循环。

这边不太好理解,我搜了几个代码案例:

 from greenlet import greenlet, GreenletExit
 huge = []
 def show_leak():
     def test1():
         gr2.switch()
 
     def test2():
         huge.extend([x* x for x in range(100)])
         gr1.switch()
         print 'finish switch del huge'
         del huge[:]
     
     gr1 = greenlet(test1)
     gr2 = greenlet(test2)
     gr1.switch()
     gr1 = gr2 = None
     print 'length of huge is zero ? %s' % len(huge)
 
 if __name__ == '__main__':
     show_leak() 
    # output: length of huge is zero ? 100

test2函数中 第11行,我们将huge清空,然后再第16行将gr1、gr2的引用计数降到了0。但运行结果告诉我们,第11行并没有执行,所以如果一个协程没有正常结束是很危险的,往往不符合程序员的预期。greenlet提供了解决这个问题的办法,如果一个greenlet实例的引用计数变成0,那么会在上次挂起的地方抛出GreenletExit异常,这就使得我们可以通过try ... finally 处理资源泄露的情况。如下面的代码:

 from greenlet import greenlet, GreenletExit
 huge = []
 def show_leak():
     def test1():
         gr2.switch()
 
     def test2():
         huge.extend([x* x for x in range(100)])
         try:
             gr1.switch()
         finally:
             print 'finish switch del huge'
             del huge[:]
     
     gr1 = greenlet(test1)
     gr2 = greenlet(test2)
     gr1.switch()
     gr1 = gr2 = None
     print 'length of huge is zero ? %s' % len(huge)
 
 if __name__ == '__main__':
     show_leak()
     # output :
     # finish switch del huge
    # length of huge is zero ? 0

上述代码的switch流程:main greenlet --> gr1 --> gr2 --> gr1 --> main greenlet, 很明显gr2没有正常结束(在第10行刮起了)。第18行之后gr1,gr2的引用计数都变成0,那么会在第10行抛出GreenletExit异常,因此finally语句有机会执行。同时,在文章开始介绍Greenlet module的时候也提到了,GreenletExit这个异常并不会抛出到parent,所以main greenlet也不会出异常。

  看上去貌似解决了问题,但这对程序员要求太高了,百密一疏。所以最好的办法还是保证协程的正常结束。


搞定了greenlet,接下来就可以看看gevent了。其实重点是概念,协程我认为就是自己控制切换的时机,在等待的时候切出去,好了之后然后在相同的地方回来继续。这一点从yield到yield from再到现在的greenlet都是一个道理。后面再看看gevent。