Greenlet初识

翻译自官方文档greenlet

什么是greenlet

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

greenlet是通过C扩展实现的。

示例

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

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    # 忽略当前的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当前暂停的地方。我们使用协程,实际上就是创建了一系列这样帧栈,然后在它们之间跳转执行。而跳转必须是明确的,跳转也称为'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并没有被打印出。

父协程

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

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

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

协程类

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

greenlet(run=None, parent=None)

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

greenlet.getcurrent()

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

greenlet.GreenletExit

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

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

切换

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

g.switch(*args, **kwargs)

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

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

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

协程的方法和属性

g.switch(*args, **kwargs)

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

g.run

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • 原文:https://greenlet.readthedocs.io/en/latest/ 背景 greenlet...
    林湾村龙猫阅读 1,176评论 0 4
  • 在之前,我已经在两篇文章中分别介绍了gevent的使用以及gevent的底层greenlet的使用,可以阅读文章回...
    WolfLC阅读 3,224评论 0 7
  • 前述 进程 线程 协程 异步 并发编程(不是并行)目前有四种方式:多进程、多线程、协程和异步。 多进程编程在pyt...
    softlns阅读 6,325评论 2 24
  • 参考资料 http://www.gevent.org/contents.html https://uwsgi-do...
    JunChow520阅读 16,827评论 0 10
  • 进程 线程 协程 异步 并发编程(不是并行)目前有四种方式:多进程、多线程、协程和异步。 多进程编程在python...
    hugoren阅读 4,941评论 1 4