聊一聊 greenlet
在使用 gunicorn 部署项目的时候,了解到很多知名的网络并发框架(gevent, eventlet)都是基于 greenlet 实现的,今天就来学习一下 greenlet 框架的原理和使用
简单使用
这里写一个简单的例子
1 | from greenlet import greenlet |
output:
1 | 12 |
在上述代码中,我们定义了两个方法类,并构建了 greenlet 协程。根据结果分析首先 gr1.switch , gr1 被执行,然后 gr2.swith 切换到 gr2 执行,打印 56,最后切换到 gr1, 打印 34,函数 run_1 返回,同时程序退出。于是 78 就不会被打印。
很好理解吧。使用switch()
方法切换协程,也比”yield”, “next/send”组合要直观的多。上例中,我们也可以看出,greenlet协程的运行,其本质是串行的,所以它不是真正意义上的并发,因此也无法发挥CPU多核的优势,不过,这个可以通过协程+进程组合的方式来解决,本文就不展开了。另外要注意的是,在没有进行显式切换时,部分代码是无法被执行到的,比如上例中的print 78
。
父子关系
在使用 greenlet 创建一个协程时,其实是有两个参数是可选的 greenlet(run=None, parent=None)
。参数 run 就是其要调用的方法,比如上例中的函数 run_1 和 run_2;parent 参数定义该协程的父协程;
参考文档:https://greenlet.readthedocs.io/en/latest/api.html#greenlets
parent 参数若不设置或为空,则其父进程就是程序默认的 main 主协程。
1 | from greenlet import greenlet |
output:
1 | 12 |
上述代码中,指定了 gr2 的父协程是 gr1,于是当最后切换至 gr2 时,程序并未退出,而是切换到了 gr1 中,打印了 34 ,最后打印 ending…
异常
协程是存放在调用栈中,那一个协程要抛出异常,就会先抛到其父协程中,如果所有的父协程都不捕获此异常,程序才会退出。
上代码
既然协程是存放在栈中,那一个协程要抛出异常,就会先抛到其父协程中,如果所有父协程都不捕获此异常,程序才会退出。我们试下,把上面的例子中函数test2()
的代码改为:
1 | def test2(): |
程序执行后,我们可以看到Traceback信息:
1 | File "parent.py", line 14, in <module> |
同时大家可以试下,如果将gr2
的父协程设为空,Traceback信息就会变为:
1 | File "parent.py", line 14, in <module> |
因此,如果gr2
的父协程是gr1
的话,异常先回抛到函数test1()
的代码gr2.switch()
处。所以,我们再对函数test1()
改动下:
1 | def test1(): |
运行后的结果,如果gr2
的父协程是gr1
,则异常被捕获,并打印”90”。否则,异常会被抛出。以上实验很好的证明了,子协程抛出的异常会根据栈里的顺序,依次抛到父协程里。
有一个异常是特例,不会被抛到父协程中,那就是greenlet.GreenletExit
,这个异常会让当前协程强制退出。比如,我们将函数test2()
改为:
1 | def test2(): |
那代码行print 78
永远不会被执行。但这个异常不会往上抛,所以其父协程还是可以正常运行。
另外,我们可以通过greenlet对象的throw()
方法,手动往一个协程里抛个异常。比如,我们在test1()
里调一个throw()
方法:
1 | def test1(): |
这样,异常就会被抛出,运行后的Trackback是这样的:
1 | File "exception.py", line 21, in <module> |
如果将gr2.throw(NameError)
放在”try”语句中,那该异常就会被捕获,并打印”90”。另外,当gr2
的父协程不是gr1
而是”main”时,异常会直接抛到主程序中,此时函数test1()
中的”try”语句就不起作用了。
协程间的消息传递
我们知道生成器是使用 send 方法来传递参数的。greenlet 也同样支持,只要在其 switch()
方法调用时,传入参数即可。
1 | from greenlet import greenlet |
output:
1 | 12 |
使用协程来构建生产者消费者:
1 | from greenlet import greenlet |