使用Python中的greenlet包实现并发编程的入门教程
1 动机
greenlet包是Stackless的副产品,其将微线程称为“tasklet”。tasklet运行在伪并发中,使用channel进行同步数据交换。
一个”greenlet”,是一个更加原始的微线程的概念,但是没有调度,或者叫做协程。这在你需要控制你的代码时很有用。你可以自己构造微线程的调度器;也可以使用”greenlet”实现高级的控制流。例如可以重新创建构造器;不同于Python的构造器,我们的构造器可以嵌套的调用函数,而被嵌套的函数也可以yield一个值。(另外,你并不需要一个”yield”关键字,参考例子)。
Greenlet是作为一个C扩展模块给未修改的解释器的。
1.1 例子
假设系统是被控制台程序控制的,由用户输入命令。假设输入是一个个字符的。这样的系统有如如下的样子:
defprocess_commands(*args): whileTrue: line='' whilenotline.endswith('\n'): line+=read_next_char() ifline=='quit\n': print"areyousure?" ifread_next_char()!="y": continue#忽略指令 process_commands(line)
现在假设你要把程序移植到GUI,而大多数GUI是事件驱动的。他们会在每次的用户输入时调用回调函数。这种情况下,就很难实现read_next_char()函数。我们有两个不兼容的函数:
defevent_keydown(key):
??
defread_next_char():
??需要等待event_keydown()的调用
你可能在考虑用线程实现。而Greenlet是另一种解决方案,没有锁和关闭问题。你启动process_commands()函数,分割成greenlet,然后与按键事件交互,有如:
defevent_keydown(key): g_processor.switch(key) defread_next_char(): g_self=greenlet.getcurrent() next_char=g_self.parent.switch()#跳到上一层(main)的greenlet,等待下一次按键 returnnext_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也会因为一个未捕捉的异常死掉。
例如:
frompy.magicimportgreenlet deftest1(): print12 gr2.switch() print34 deftest2(): print56 gr1.switch() print78 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之间传递信息的方便方式。例如:
deftest1(x,y): z=gr2.switch(x+y) printz deftest2(u): printu gr1.switch(42) gr1=greenlet(test1) gr2=greenlet(test2) gr1.switch("hello","world")
这会打印出“helloworld”和42,跟前面的例子的输出顺序相同。注意test1()和test2()的参数并不是在greenlet创建时指定的,而是在第一次切换到这里时传递的。
这里是精确的调用方式:
g.switch(obj=Noneor*args)
切换到执行点greenletg,发送给定的对象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,或者父的父,等等。最终的父就是maingreenlet,永远不会死掉的。
2.5 greenlet的方法和属性
g.switch(obj=Noneor*args)
切换执行点到greenletg,同上。
g.run
调用可执行的g,并启动。在g启动后,这个属性就不再存在了。
g.parent
greenlet的父。这是可写的,但是不允许创建循环的父关系。
g.gr_frame
当前顶级帧,或者None。
g.dead
判断是否已经死掉了
bool(g)
如果g是活跃的则返回True,在尚未启动或者结束后返回False。
g.throw([typ,[val,[tb]]])
切换执行点到greenletg,但是立即抛出指定的异常到g。如果没有提供参数,异常缺省就是greenlet.GreenletExit。根据异常波及规则,有如上面描述的。注意调用这个方法等同于如下:
defraiser(): raisetyp,val,tb g_raiser=greenlet(raiser,parent=g) g_raiser.switch()
2.6 Greenlet与Python线程
greenlet可以与Python线程一起使用;在这种情况下,每个线程包含一个独立的maingreenlet,并拥有自己的greenlet树。不同线程之间不可以互相切换greenlet。
2.7 活动greenlet的垃圾收集
如果不再有对greenlet对象的引用时(包括其他greenlet的parent),还是没有办法切换回greenlet。这种情况下会生成一个GreenletExit异常到greenlet。这是greenlet收到异步异常的唯一情况。应该给出一个try..finally用于清理greenlet内的资源。这个功能同时允许greenlet中无限循环的编程风格。这样循环可以在最后一个引用消失时自动中断。
如果不希望greenlet死掉或者把引用放到别处,只需要捕捉和忽略GreenletExit异常即可。
greenlet不参与垃圾收集;greenlet帧的循环引用数据会被检测到。将引用传递到其他的循环greenlet会引起内存泄露。