轻量级并发程序,使用Python中的greenlet包完毕产出
分类:计算机编程

英文原文地址:
中文翻译转载地址:

使用Python中的greenlet包实现并发编程的入门教程,pythongreenlet

1   动机

greenlet 包是 Stackless 的副产品,其将微线程称为 “tasklet” 。tasklet运行在伪并发中,使用channel进行同步数据交换。

一个”greenlet”,是一个更加原始的微线程的概念,但是没有调度,或者叫做协程。这在你需要控制你的代码时很有用。你可以自己构造微线程的 调度器;也可以使用”greenlet”实现高级的控制流。例如可以重新创建构造器;不同于Python的构造器,我们的构造器可以嵌套的调用函数,而被 嵌套的函数也可以 yield 一个值。(另外,你并不需要一个”yield”关键字,参考例子)。

Greenlet是作为一个C扩展模块给未修改的解释器的。

1.1   例子

假设系统是被控制台程序控制的,由用户输入命令。假设输入是一个个字符的。这样的系统有如如下的样子:

def process_commands(*args):
  while True:
    line=''
    while not line.endswith('n'):
      line =read_next_char()
    if line=='quitn':
      print "are you sure?"
      if read_next_char()!="y":
        continue  #忽略指令
    process_commands(line)

现在假设你要把程序移植到GUI,而大多数GUI是事件驱动的。他们会在每次的用户输入时调用回调函数。这种情况下,就很难实现 read_next_char() 函数。我们有两个不兼容的函数:

def event_keydown(key):
    ??

def read_next_char():
    ?? 需要等待 event_keydown() 的调用

你可能在考虑用线程实现。而 Greenlet 是另一种解决方案,没有锁和关闭问题。你启动 process_commands() 函数,分割成 greenlet ,然后与按键事件交互,有如:

def event_keydown(key):
  g_processor.switch(key)

def read_next_char():
  g_self=greenlet.getcurrent()
  next_char=g_self.parent.switch()  #跳到上一层(main)的greenlet,等待下一次按键
  return next_char

g_processor=greenlet(process_commands)
g_processor.switch(*args)
gui.mainloop()

这个例子的执行流程是: read_next_char() 被调用,也就是 g_processor 的一部分,它就会切换(switch)到他的父greenlet,并假设继续在顶级主循环中执行(GUI主循环)。当GUI调用 event_keydown() 时,它切换到 g_processor ,这意味着执行会跳回到原来挂起的地方,也就是 read_next_char() 函数中的切换指令那里。然后 event_keydown() 的 key 参数就会被传递到 read_next_char() 的切换处,并返回。

注意 read_next_char() 会被挂起并假设其调用栈会在恢复时保护的很好,所以他会在被调用的地方返回。这允许程序逻辑保持优美的顺序流。我们无需重写 process_commands() 来用到一个状态机中。

2   使用

2.1   简介

一个 “greenlet” 是一个很小的独立微线程。可以把它想像成一个堆栈帧,栈底是初始调用,而栈顶是当前greenlet的暂停位置。你使用greenlet创建一堆这样的堆 栈,然后在他们之间跳转执行。跳转不是绝对的:一个greenlet必须选择跳转到选择好的另一个greenlet,这会让前一个挂起,而后一个恢复。两 个greenlet之间的跳转称为 切换(switch) 。

当你创建一个greenlet,它得到一个初始化过的空堆栈;当你第一次切换到它,他会启动指定的函数,然后切换跳出greenlet。当最终栈底 函数结束时,greenlet的堆栈又编程空的了,而greenlet也就死掉了。greenlet也会因为一个未捕捉的异常死掉。

例如:

from py.magic 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是不会被打印的。

2.2   父greenlet

现在看看一个greenlet死掉时执行点去哪里。每个greenlet拥有一个父greenlet。父greenlet在每个greenlet初 始化时被创建(不过可以在任何时候改变)。父greenlet是当greenlet死掉时,继续原来的位置执行。这样,greenlet就被组织成一棵 树,顶级的代码并不在用户创建的 greenlet 中运行,而称为主greenlet,也就是树根。

在上面的例子中,gr1和gr2都是把主greenlet作为父greenlet的。任何一个死掉,执行点都会回到主函数。

未捕获的异常会波及到父greenlet。如果上面的 test2() 包含一个打印错误(typo),他会生成一个 NameError 而干掉gr2,然后执行点会回到主函数。traceback会显示 test2() 而不是 test1() 。记住,切换不是调用,但是执行点可以在并行的栈容器间并行交换,而父greenlet定义了栈最初从哪里来。

2.3   实例

py.magic.greenlet 是一个 greenlet 类型,支持如下操作:

greenlet(run=None,parent=None)

    创建一个greenlet对象,而不执行。run是执行回调,而parent是父greenlet,缺省是当前greenlet。

greenlet.getcurrent()

    返回当前greenlet,也就是谁在调用这个函数。

greenlet.GreenletExit

    这个特定的异常不会波及到父greenlet,它用于干掉一个greenlet。

greenlet 类型可以被继承。一个greenlet通过调用其 run 属性执行,就是创建时指定的那个。对于子类,可以定义一个 run() 方法,而不必严格遵守在构造器中给出 run 参数。

2.4   切换

greenlet之间的切换发生在greenlet的 switch() 方法被调用时,这会让执行点跳转到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(obj=None or *args)

切换到执行点greenlet g,发送给定的对象obj。在特殊情况下,如果g还没有启动,就会让它启动;这种情况下,会传递参数过去,然后调用 g.run(*args) 。

垂死的greenlet

    如果一个greenlet的 run() 结束了,他会返回值到父greenlet。如果 run() 是异常终止的,异常会波及到父greenlet(除非是 greenlet.GreenletExit 异常,这种情况下异常会被捕捉并返回到父greenlet)。

除了上面的情况外,目标greenlet会接收到发送来的对象作为 switch() 的返回值。虽然 switch() 并不会立即返回,但是它仍然会在未来某一点上返回,当其他greenlet切换回来时。当这发生时,执行点恢复到 switch() 之后,而 switch() 返回刚才调用者发送来的对象。这意味着 x=g.switch(y) 会发送对象y到g,然后等着一个不知道是谁发来的对象,并在这里返回给x。

注意,任何尝试切换到死掉的greenlet的行为都会切换到死掉greenlet的父greenlet,或者父的父,等等。最终的父就是 main greenlet,永远不会死掉的。

2.5   greenlet的方法和属性

g.switch(obj=None or *args)

    切换执行点到greenlet g,同上。

g.run

    调用可执行的g,并启动。在g启动后,这个属性就不再存在了。

g.parent

    greenlet的父。这是可写的,但是不允许创建循环的父关系。

g.gr_frame

    当前顶级帧,或者None。

g.dead

    判断是否已经死掉了

bool(g)

    如果g是活跃的则返回True,在尚未启动或者结束后返回False。

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

    切换执行点到greenlet g,但是立即抛出指定的异常到g。如果没有提供参数,异常缺省就是 greenlet.GreenletExit 。根据异常波及规则,有如上面描述的。注意调用这个方法等同于如下:

  def raiser():
    raise typ,val,tb

  g_raiser=greenlet(raiser,parent=g)
  g_raiser.switch()

2.6   Greenlet与Python线程

greenlet可以与Python线程一起使用;在这种情况下,每个线程包含一个独立的 main greenlet,并拥有自己的greenlet树。不同线程之间不可以互相切换greenlet。

2.7   活动greenlet的垃圾收集

如果不再有对greenlet对象的引用时(包括其他greenlet的parent),还是没有办法切换回greenlet。这种情况下会生成一个 GreenletExit 异常到greenlet。这是greenlet收到异步异常的唯一情况。应该给出一个 try .. finally 用于清理greenlet内的资源。这个功能同时允许greenlet中无限循环的编程风格。这样循环可以在最后一个引用消失时自动中断。

如果不希望greenlet死掉或者把引用放到别处,只需要捕捉和忽略 GreenletExit 异常即可。

greenlet不参与垃圾收集;greenlet帧的循环引用数据会被检测到。将引用传递到其他的循环greenlet会引起内存泄露。

1 动机 greenlet 包是 Stackless 的副产品,其将微线程称为 “tasklet” 。tasklet运行...

翻译自官方文档greenlet。

背景

greenlet包是Stackless的衍生产品,它是一个支持微线程(叫tasklets)的CPython版本。Tasklets运行在伪并发模式下(通常在一个或少许的OS级别的线程),他们通过“channels”来交互数据。

另一方面来说, 一个“greenlet”任然是一个没有内部调度的关于微线程的较为原始的概念。换句话说,当你想要在你代码运行时做到准确控制,“greenlet”是一种很有用的方式。在greenlet基础之上,你可以定义自己的微线程调度策略。不管怎样,greenlets也可以以一种高级控制流结构的方式用于他们自己。举个例子,我们可以重新生成迭代器。python自带的生成器与greenlet的生成器之间的区别是greenlet的生成器可以嵌套调用函数,并且嵌套函数也会yield值(补充说明的是,你不需要使用yield关键词,参见例子:test_generator.py)。

什么是greenlet

greenlet是从Stackless中分离的项目。greenlet也叫微线程、协程,它的调度是由程序明确控制的,所以执行流程是固定的、明确的。而线程的调度完全交由操作系统,执行顺序无法预料。同时,协程之间切换的代价远比线程小。

greenlet是通过C扩展实现的。

例子

我们来考虑一个用户输入命令的终端控制台系统。假设输入是逐个字符输入。在这样的一个系统中,有个典型的循环如下所示:

def process_commands(*args):
    while True:
        line = ''
        while not line.endswith('n'):
            line  = read_next_char()
        if line == 'quitn':
            print("are you sure?")
            if read_next_char() != 'y':
                continue    # ignore the command
        process_command(line)

现在,假设你将程序移植到GUI程序中,绝大部分的GUI成套工具是基于事件驱动的。他们为每一个用户字符输入调用一个回调函数。(将“GUI”替换成“XML expat parser”,对你来说应该更加熟悉了)。在这样的情形中,执行下面的函数read_next_char()是很困难的。这里是两个不兼容的函数:

def event_keydown(key):
    ??

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

你可能考虑用线程的方式来实现这个了。greenlets是另一种不需要关联锁与没有当机问题的可选的解决方案。你执行process_commands(),独立的greenlet。通过如下方式输入字符串。

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()

这个例子中,执行流程如下:

  • 当作为g_processor greenlet一部分的read_next_char()函数被调用,所以当接收到输入切换到上级greenlet, 程序恢复到主循环(GUI)执行。
  • 当GUI调用event_keydown()的时候,程序切换到g_processor。这就意味着程序跳出,无论它被挂起在这个greenlet什么地方。在这个例子中,切换到read_next_char(),并且在event_keydown()中被按下的key作为switch()的结果返回给了read_next_char()。

需要说明的是read_next_char()的挂起与恢复都保留其调用堆栈。以便在prprocess_commands()中根据他来的地方恢复到不同的位置。这使得以一种好的控制流来控制程序逻辑成为可能。我们不必完整的重写process_commands(),将其转换为状态机。

示例

有这么一个系统,它根据用户在终端输入命令的不同而执行不同的操作,假设输入是逐字符的。部分代码可能是这样的:

def process_commands(*args):
    while True:
        line = ''
        while not line.endswith('n'):
            line  = read_next_char()
        if line == 'quitn':
            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” 是微型的独立的伪线程。考虑到作为一个帧堆栈。最远的帧(最底层)是你调用的最初的函数,最外面的帧(最顶层)是在当前greenlet被压进去的。当你使用greenlets的时候是通过创建一系列的这种堆栈,然后在他们之间跳转执行。这种跳转将会导致先前的帧挂起,最后的帧从挂起状态恢复。在greenlets之间的跳转关系叫做“switching(切换)”。

当你创建一个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完成,并且gr1死亡。与此同时,程序执行返回到gr1.switch()调用。
  • 需要说明的是78从来都没有打印。

简介

一个协程是一个独立的“假线程”。可以把它想成一个小的帧栈,栈底是你调用的初始函数,栈顶是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并没有被打印出。

父级greenlet

让我们看看当greenlet死亡的时候,程序执行到哪里去了。每一个greenlet都有一个父级greenlet。最初的父级是创建greenlet的那一个greenlet(父级greenlet是可以在任何时候被改变)。父级greenlet是当一个greenlet死亡的时候程序继续执行的地方。这种方式,程序组织成一颗树。不在用户创建的greenlet中运行的顶层代码在隐式的主greenlet中运行,它是堆栈数的根。

在上面的例子中,gr1与gr2将主greenlet作为父级greenlet。无论它们中的谁执行完毕,程序执行都会返回到"main"greenlet中。

没有捕获的异常将抛出到父级greenlet中。举个例子,如果上面的test2()包含一个语法错误,它将生成一个杀死gr2的NameError错误,这个错误将直接跳转到主greenlet。错误堆栈将显示test2,而不会是test1。需要注意的是,switches不是调用,而是程序在并行的"stack container(堆栈容器)"直接执行的跳转,“parent”定义了逻辑上位于当前greenlet之下的堆栈。

父协程

每个协程都有一个父协程。协程在哪个协程中被创建,那么这个协程就是父协程,当然后面可以更改。当某个协程死亡后,会在父协程中继续执行。举个例子,在g1中创建了g2,那么g1就是g2的父协程,g2死亡后,会在g1中继续执行。这么说的话,协程是树结构的。最上层的代码不是运行在用户定义的协程中,而是在一个隐式的主协程中,它是协程树的根(root)。

在上面的例子中,gr1和gr2的父协程都是主协程。不管哪一个死亡,执行都会回到主协程。

异常也会被传到父协程。比如说,test2中若包含了一个'typo',就会引发NameError异常,然后杀死gr2,执行会直接回到主协程。Traceback会显示test2而不是test1。注意,协程的切换不是调用,而是在平行的"栈容器"中传递执行。

实例化对象

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方法比提供一个run参数给构造器更有意义。

协程类

greenlet.greenlet就是协程类,它支持下面一些操作:

切换

当在一个greenlet中调用方法switch(),在greenlet之间的切换将发生,正常情况下,程序执行跳转到switch()被调用的greenlet中。或者当一个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没有执行,它将开始执行。

对于将死的greenlet。当run()完成的时候,将会发生对象给父级greenlet。如果greenlet因为异常而终止,这个异常将会抛出到父级greenlet中(greenlet.GreenletExit例外,这个异常被捕获了并且直接退出到父级greenlet中)。

除了上面例子描述的,通常目标greenlet(父级greenlet)接收之前调用switch()挂起,执行完毕返回的返回值作为结果。事实上,虽然对switch()的调用不会立即返回结果,但是当其他一些greenlet切换回来的时候,在将来的某个点将会返回结果。当切换发生的时候,程序将在它之前挂起的地方恢复。switch()自己返回发生的对象。这就意味着x=g.switch(y)将y给g,稍后将返回从某个不关联的greenlet中返回的不关联的对象给x变量。

提醒一下,任何试图切换到一个死亡的greenlet的将会走到死亡greenlet的父级,或者父级的父级,以此类推(最终的父级是“main” greenlet,它是从来不会死掉的)。

greenlet(run=None, parent=None)

创建一个新的协程对象。run是一个可调用对象,parent是父协程,默认是当前协程。

greenlets的方法与属性

  • g.switch(*args, **kwargs):切换程序到greenlet g中执行,参见上面。
  • g.run:当它开始的时候,g的回调将会被执行,当g已经开始执行了,这个属性将不会存在了。
  • g.parent:父级greenlet。这是可编辑属性,但是不能够写成了死循环。
  • g.gr_frame:最顶层的结构,或者等于None。
  • g.dead: bool值,当g死亡了,值为True。
  • bool(g):bool值,当返回结构是True,表示g还活跃,如果是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异常,这个异常将不会从g_raiser传播到g。

greenlet.getcurrent()

返回当前协程,也就是调用这个函数的协程。

Greenlets与python的线程

Greenlets将可以和python线程结合起来。这种情况下,每一个线程包含一个独立的带有一个子greenlets树的“main” greenlet。混合或切换在不同线程中的greenlets是不可能的事情。

greenlet.GreenletExit

这个特殊的异常不会传给父协程,常用来杀死协程。

greenlet是可以被继承的。协程通过执行run属性来运行。在子类中,可以自由地去定义run,而不是一定要传递run参数给构造器。

greenlets的垃圾回收生命周期

如果对一个greenlet的所有关联都已经失效(包括来自其他greenlets中的父级属性的关联),这时候,没有任何一种方式切换回该greenlet中。这种情况下,GreenletExit异常将会产生。这是一个greenlet接受异步执行的唯一方式。使用try:finally:语句块来清理被这个greenlet使用的资源。这种属性支持一种编程风格,greenlet无限循环等待数据并且执行。当对该greenlet的最后关联失效,这种循环将自动终止。

如果greenlet要么死亡,要么根据存在某个地方的关联恢复。只需要捕获与忽略可能导致无限循环的GreenletExit。

Greenlets不参与垃圾回收。循环那些在greenlet框架中的数据时候,这些数据将不会被检测到。循环的存储其他greenlets的引用将可能导致内存泄漏。

切换

有两种情况会发生协程之间的切换。一是某个协程主动调用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在协程创建时并没有提供参数,而是在第一次切换的地方。

错误堆栈支持

当使用greenlet的时候,标准的python错误堆栈与描述将不会按照预期的运行,因为堆栈与框架的切换发生在相同的线程中。使用传统的方法可靠的检测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,而不是其他可能的事件时候,将参数解包成tuple。这样,API可能扩展出于sys.settrace()相似的新的事件。

g.switch(*args, **kwargs)

切换到协程g执行,传递提供的参数。如果g还没运行,那么传递参数给g的run属性,并开始执行run()。

如果协程的run()执行结束,return的值会返回给主协程。如果run()以异常方式结束,异常会传递给主协程(除非是greenlet.GreenletExit,这种情况下会直接返回到主协程)。

如果切换到一个已死亡的的协程,那么实际上是切换到它的父协程,依次类推。

C API 相关

Greenlets可以通过用C/C 写的扩展模块来生成与维护,或者来自于嵌入到python中的应用。greenlet.h 头文件被提供,用来展示对原生的python模块的完整的API访问。

协程的方法和属性

类型

Type namePython namePyGreenletgreenlet.greenlet

g.switch(*args, **kwargs)

切换到协程g执行,见上面。

异常

Type namePython namePyExc_GreenletErrorgreenlet.errorPyExc_GreenletExitgreenlet.GreenletExit

g.run

一个可调用对象,当g开始执行时,调用它。但是一旦开始执行后,这个属性就不存在了。

关联

  • PyGreenlet_Import():一个宏定义,导入greenlet模块,初始化C API。必须在每一个用到greenlet C API的模块中调用一次。
  • int PyGreenlet_Check(PyObject *p):一个宏定义,如果参数是PyGreenlet返回true。
  • int PyGreenlet_STARTED(PyGreenlet *g):一个宏定义,如果greenlet在开始了返回true。
  • int PyGreenlet_ACTIVE(PyGreenlet *g):一个宏定义,如果greenlet在活动中返回true。
  • PyGreenlet *PyGreenlet_GET_PARENT(PyGreenlet *g):一个宏定义,返回greenlet中的父级greenlet。
  • int PyGreenlet_SetParent(PyGreenlet *g, PyGreenlet *nparent):设置父级greenlet。返回0为设置成功,-1,表示g不是一有效的PyGreenlet指针,AttributeError将抛出。
  • PyGreenlet *PyGreenlet_GetCurrent(void):返回当前活跃的greenlet对象。
  • PyGreenlet *PyGreenlet_New(PyObject *run, PyObject *parent):使用run与parent创建一个新的greenlet对象。这两个参数是可选的。如果run是NULL。这个greenlet创建,如果切换开始将失败。如果parent是NULL。这个parent将自动设置成当前greenlet。
  • PyObject *PyGreenlet_Switch(PyGreenlet *g, PyObject *args, PyObject *kwargs):切换到greenet g。args与kwargs是可选的,可以为NULL。如果args为NULL,一个空的tuple将发送给目标greenlet g。如果kwargs是NULL的。没有key-value参数发送。如果指定参数,那么args应该是一个tuple,kwargs应该是一个dict。
  • PyObject *PyGreenlet_Throw(PyGreenlet *g, PyObject *typ, PyObject *val, PyObject *tb):切换到greenlet g,并且立马抛出typ参数(携带的值val)指定的异常,调用堆栈对象tb是可选的,并且可以为NULL。
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

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

本文由pc28.am发布于计算机编程,转载请注明出处:轻量级并发程序,使用Python中的greenlet包完毕产出

上一篇:的报道方式计算,如鹏网学习笔记 下一篇:没有了
猜你喜欢
热门排行
精彩图文