Greenlet 详解

本文内容主要来自对官网文档的翻译,在其中也加入了不少本身的理解和例子。主要包括如下内容:什么是greenlet,greenlet的切换与函数调用的区别,greenlet的生命周期,以及使用greenlet的注意事项。html

 

greenlet初体验python

 

Greenlet是python的一个C扩展,来源于Stackless python,旨在提供可自行调度的‘微线程’, 即协程。generator实现的协程在yield value时只能将value返回给调用者(caller)。 而在greenlet中,target.switch(value)能够切换到指定的协程(target), 而后yield value。greenlet用switch来表示协程的切换,从一个协程切换到另外一个协程须要显式指定。程序员

 

greenlet的安装很简单:pip install greenlet 便可,安装好了以后咱们来看一个官方的例子:less

 

from greenlet import greenletssh

def test1():异步

    print 12函数

    gr2.switch()学习

    print 34线程

 

def test2():翻译

    print 56

    gr1.switch()

    print 78

 

gr1 = greenlet(test1)

gr2 = greenlet(test2)

gr1.switch()

 

输出为:

 

12 56 34

 

当建立一个greenlet时,首先初始化一个空的栈, switch到这个栈的时候,会运行在greenlet构造时传入的函数(首先在test1中打印 12), 若是在这个函数(test1)中switch到其余协程(到了test2 打印34),那么该协程会被挂起,等到切换回来(在test2中切换回来 打印34)。当这个协程对应函数执行完毕,那么这个协程就变成dead状态。

 

注意:上面没有打印test2的最后一行输出 78,由于在test2中切换到gr1以后挂起,可是没有地方再切换回来。这个可能形成泄漏,后面细说。

 

greenlet module与class

 

咱们首先看一下greenlet这个module里面的属性:

 

>> dir(greenlet)

['GREENLET_USE_GC', 'GREENLET_USE_TRACING', 'GreenletExit', '_C_API', '__doc__', '__file__', '__name__','__package__', '__version__', 'error', 'getcurrent', 'gettrace', 'greenlet', 'settrace']

>>>

 

其中,比较重要的是getcurrent(), 类greenlet、异常类GreenletExit。

 

getcurrent()返回当前的greenlet实例;

 

GreenletExit:是一个特殊的异常,当触发了这个异常的时候,即便不处理,也不会抛到其parent(后面会提到协程中对返回值或者异常的处理)

 

而后咱们再来看看greenlet.greenlet这个类:

 

>>> dir(greenlet.greenlet)

['GreenletExit', '__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__getstate__', '__hash__','__init__', '__new__', '__nonzero__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__',   '__sizeof__', '__str__','__subclasshook__', '_stack_saved', 'dead', 'error', 'getcurrent', 'gettrace', 'gr_frame', 'parent', 'run', 'settrace','switch','throw']

>>>

 

比较重要的几个属性:

 

  • run:当greenlet启动的时候会调用到这个callable,若是咱们须要继承greenlet.greenlet时,须要重写该方法

  • switch:前面已经介绍过了,在greenlet之间切换

  • parent:可读写属性,后面介绍

  • dead:若是greenlet执行结束,那么该属性为true

  • throw:切换到指定greenlet后当即跑出异常

 

文章后面提到的greenlet大多都是指greenlet.greenlet这个class,请注意区别。

 

Switch not call

 

对于greenlet,最经常使用的写法是 x = gr.switch(y)。 这句话的意思是切换到gr,传入参数y。当从其余协程(不必定是这个gr)切换回来的时候,将值付给x。

 

import greenlet

def test1(x, y):

    z = gr2.switch(x+y)

    print('test1 ', z)

 

def test2(u):

    print('test2 ', u)

    gr1.switch(10)

 

gr1 = greenlet.greenlet(test1)

gr2 = greenlet.greenlet(test2)

print gr1.switch("hello", " world")

 

输出:

 

(‘test2 ‘, ‘hello world’)

(‘test1 ‘, 10)

None

 

上面的例子,第12行从main greenlet切换到了gr1,test1第3行切换到了gs2,而后gr1挂起,第8行从gr2切回gr1时,将值(10)返回值给了 z。

 

每个Greenlet都有一个parent,一个新的greenlet在哪里创生,当前环境的greenlet就是这个新greenlet的parent。全部的greenlet构成一棵树,其跟节点就是尚未手动建立greenlet时候的”main” greenlet(事实上,在首次import greenlet的时候实例化)。当一个协程 正常结束,执行流程回到其对应的parent;或者在一个协程中抛出未被捕获的异常,该异常也是传递到其parent。学习python的时候,有一句话会被无数次重复”everything is oblect”, 在学习greenlet的调用中,一样有一句话应该深入理解,“switch not call”。

 

import greenlet

def test1(x, y):

    print id(greenlet.getcurrent()), id(greenlet.getcurrent().parent) # 40240272 40239952

    z = gr2.switch(x+y)

    print 'back z', z

 

def test2(u):

    print id(greenlet.getcurrent()), id(greenlet.getcurrent().parent) # 40240352 40239952

    return 'hehe'

 

gr1 = greenlet.greenlet(test1)

gr2 = greenlet.greenlet(test2)

print id(greenlet.getcurrent()), id(gr1), id(gr2)     # 40239952, 40240272, 40240352

print gr1.switch("hello", " world"), 'back to main'    # hehe back to main

 

上述例子能够看到,尽可能是从test1所在的协程gr1 切换到了gr2,但gr2的parent仍是’main’ greenlet,由于默认的parent取决于greenlet的创生环境。另外 在test2中return以后整个返回值返回到了其parent,而不是switch到该协程的地方(即不是test1),这个跟咱们平时的函数调用不同,记住“switch not call”。对于异常 也是展开至parent。

 

import greenlet

def test1(x, y):

    try:

        z = gr2.switch(x+y)

    except Exception:

        print 'catch Exception in test1'

 

def test2(u):

    assert False

 

gr1 = greenlet.greenlet(test1)

gr2 = greenlet.greenlet(test2)

try:

    gr1.switch("hello", " world")

except:

    print 'catch Exception in main'

 

输出为:

 

catch Exception in main

 

Greenlet生命周期

 

文章开始的地方提到第一个例子中的gr2其实并无正常结束,咱们能够借用greenlet.dead这个属性来查看:

 

from greenlet import greenlet

def test1():

    gr2.switch(1)

    print 'test1 finished'

 

def test2(x):

    print 'test2 first', x

    z = gr1.switch()

    print 'test2 back', z

 

gr1 = greenlet(test1)

gr2 = greenlet(test2)

gr1.switch()

print 'gr1 is dead?: %s, gr2 is dead?: %s' % (gr1.dead, gr2.dead)

gr2.switch()

print 'gr1 is dead?: %s, gr2 is dead?: %s' % (gr1.dead, gr2.dead)

print gr2.switch(10)

 

输出:

 

test2 first 1

test1 finished

gr1 is dead?: True, gr2 is dead?: False

test2 back ()

gr1 is dead?: True, gr2 is dead?: True

10

 

从这个例子能够看出:

 

  • 只有当协程对应的函数执行完毕,协程才会die,因此第一次Check的时候gr2并无die,由于第9行切换出去了就没切回来。在main中再switch到gr2的时候, 执行后面的逻辑,gr2 die。

  • 若是试图再次switch到一个已是dead状态的greenlet会怎么样呢,事实上会切换到其parent greenlet。

 

Greenlet Traceing

 

Greenlet也提供了接口使得程序员能够监控greenlet的整个调度流程。主要是gettrace 和 settrace(callback)函数。下面看一个例子:

 

def test_greenlet_tracing():

    def callback(event, args):

        print event, 'from', id(args[0]), 'to', id(args[1])

 

    def dummy():

        g2.switch()

 

    def dummyexception():

        raise Exception('excep in coroutine')

 

    main = greenlet.getcurrent()

    g1 = greenlet.greenlet(dummy)

    g2 = greenlet.greenlet(dummyexception)

    print 'main id %s, gr1 id %s, gr2 id %s' % (id(main), id(g1), id(g2))

    oldtrace = greenlet.settrace(callback)

    try:

        g1.switch()

    except:

        print 'Exception'

    finally:

        greenlet.settrace(oldtrace)

 

test_greenlet_tracing()

 

输出:

 

main id 40604416, gr1 id 40604736, gr2 id 40604816

switch from 40604416 to 40604736

switch from 40604736 to 40604816

throw from 40604816 to 40604416

Exception

 

其中callback函数event是switch或者throw之一,代表是正常调度仍是异常跑出;args是二元组,表示是从协程args[0]切换到了协程args[1]。上面的输出展现了切换流程:从main到gr1,而后到gr2,最后回到main。

 

greenlet使用建议:

 

使用greenlet须要注意一下三点:

 

第一:greenlet创生以后,必定要结束,不能switch出去就不回来了,不然容易形成内存泄露。

 

第二:python中每一个线程都有本身的main greenlet及其对应的sub-greenlet ,不能线程之间的greenlet是不能相互切换的。

 

第三:不能存在循环引用,这个是官方文档明确说明。

 

  ”Greenlets do not participate in garbage collection; cycles involving data that is present in a greenlet’s frames will not be detected. “

 

对于第一点,咱们来看一个例子:

 

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行将gr一、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也不会出异常。

 

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

 

总结:

 

以前的文章其实已经提到提到了coroutine协程的强大之处,对于异步非阻塞,并且还须要保留上下文的场景很是适用。greenlet跟强大,能够从一个协程切换到任意其余协程,这是generator作不到的,但这种能力其实也是双刃剑,前面的注意事项也提到了,必须保证greenlet的正常结束,在协程之间任意的切换很容易出问题。

 

好比对于服务之间异步请求的例子,简化为服务A的一个函数foo须要异步访问服务B,能够这样封装greenlet:用decorator装饰函数foo,当调用这个foo的时候创建一个greenlet实例,并为这个greenley对应一个惟一的gid,在foo方法发出异步请求(写到gid)以后,switch到parent,这个时候这个新的协程处于挂起状态。当请求返回以后,经过gid找到以前被挂起的协程,恢复该协程便可。More simple More safety,保证旨在main和一级子协程之间切换。须要注意的是处理各类异常 以及请求超时的状况,避免内存泄露,gvent对greenlet的使用大体也是这样的。

 

参考

 

  • http://www.cnblogs.com/xybaby/p/6323358.html

  • https://pypi.python.org/pypi/greenlet

  • https://en.wikipedia.org/wiki/Stackless_Python

  • http://greenlet.readthedocs.io/en/latest/

  • http://stackoverflow.com/questions/715758/coroutine-vs-continuation-vs-generator

相关文章
相关标签/搜索