聊一聊 greenlet

聊一聊 greenlet

在使用 gunicorn 部署项目的时候,了解到很多知名的网络并发框架(gevent, eventlet)都是基于 greenlet 实现的,今天就来学习一下 greenlet 框架的原理和使用

简单使用

这里写一个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from greenlet import greenlet


def run_1():
print(12)
gr2.switch()
print(34)


def run_2():
print(56)
gr1.switch()
print(78)


gr1 = greenlet(run_1)
gr2 = greenlet(run_2)

if __name__ == '__main__':
gr1.switch()

output:

1
2
3
4
12
56
34

在上述代码中,我们定义了两个方法类,并构建了 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from greenlet import greenlet


def run_1():
print(12)
gr2.switch()
print(34)


def run_2():
print(56)


gr1 = greenlet(run_1)
gr2 = greenlet(run_2, gr1)

if __name__ == '__main__':
gr1.switch()
print("ending")

output:

1
2
3
4
12
56
34
ending

上述代码中,指定了 gr2 的父协程是 gr1,于是当最后切换至 gr2 时,程序并未退出,而是切换到了 gr1 中,打印了 34 ,最后打印 ending…

异常

协程是存放在调用栈中,那一个协程要抛出异常,就会先抛到其父协程中,如果所有的父协程都不捕获此异常,程序才会退出。

上代码

既然协程是存放在栈中,那一个协程要抛出异常,就会先抛到其父协程中,如果所有父协程都不捕获此异常,程序才会退出。我们试下,把上面的例子中函数test2()的代码改为:

1
2
3
def test2():
print 56
raise NameError

程序执行后,我们可以看到Traceback信息:

1
2
3
4
5
6
File "parent.py", line 14, in <module>
gr1.switch()
File "parent.py", line 5, in test1
gr2.switch()
File "parent.py", line 10, in test2
raise NameError

同时大家可以试下,如果将gr2的父协程设为空,Traceback信息就会变为:

1
2
3
4
File "parent.py", line 14, in <module>
gr1.switch()
File "parent.py", line 10, in test2
raise NameError

因此,如果gr2的父协程是gr1的话,异常先回抛到函数test1()的代码gr2.switch()处。所以,我们再对函数test1()改动下:

1
2
3
4
5
6
7
def test1():
print 12
try:
gr2.switch()
except NameError:
print 90
print 34

运行后的结果,如果gr2的父协程是gr1,则异常被捕获,并打印”90”。否则,异常会被抛出。以上实验很好的证明了,子协程抛出的异常会根据栈里的顺序,依次抛到父协程里。

有一个异常是特例,不会被抛到父协程中,那就是greenlet.GreenletExit,这个异常会让当前协程强制退出。比如,我们将函数test2()改为:

1
2
3
4
def test2():
print 56
raise greenlet.GreenletExit
print 78

那代码行print 78永远不会被执行。但这个异常不会往上抛,所以其父协程还是可以正常运行。

另外,我们可以通过greenlet对象的throw()方法,手动往一个协程里抛个异常。比如,我们在test1()里调一个throw()方法:

1
2
3
4
5
6
7
8
def test1():
print 12
gr2.throw(NameError)
try:
gr2.switch()
except NameError:
print 90
print 34

这样,异常就会被抛出,运行后的Trackback是这样的:

1
2
3
4
File "exception.py", line 21, in <module>
gr1.switch()
File "exception.py", line 5, in test1
gr2.throw(NameError)

如果将gr2.throw(NameError)放在”try”语句中,那该异常就会被捕获,并打印”90”。另外,当gr2的父协程不是gr1而是”main”时,异常会直接抛到主程序中,此时函数test1()中的”try”语句就不起作用了。

协程间的消息传递

我们知道生成器是使用 send 方法来传递参数的。greenlet 也同样支持,只要在其 switch() 方法调用时,传入参数即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from greenlet import greenlet


def run_1():
print(12)
y = gr2.switch(56)
print(y)


def run_2(x):
print(x)
gr1.switch(34)
print(78)


gr1 = greenlet(run_1)
gr2 = greenlet(run_2)

if __name__ == '__main__':
gr1.switch()
print("ending")

output:

1
2
3
4
12
56
34
ending

使用协程来构建生产者消费者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from greenlet import greenlet

def consumer():
last = ''
while True:
receival = pro.switch(last)
if receival is not None:
print(f"consumer: {receival}")


def producer(n):
con.switch()
x = 0
while x < n:
x += 1
print(f"producer: {x}")
con.switch(x)


pro = greenlet(producer)
con = greenlet(consumer)
pro.switch(5)


if __name__ == '__main__':
c = greenlet(consumer)
p = greenlet(producer)
p.switch(5)

Refs

github: https://github.com/python-greenlet/greenlet

-------------THANKS FOR READING-------------