当前位置: 首页 > 工具软件 > greenlet > 使用案例 >

专题15:python中的协程库—greenlet和gevent

艾阳羽
2023-12-01

推荐阅读

greenlet官方文档:https://greenlet.readthedocs.io/en/latest/

Form:https://www.jianshu.com/u/3ab212f28d91

Python Gevent – 高性能的 Python 并发框架:https://blog.csdn.net/freeking101/article/details/53097420

Python 的协程库 greenlet 和 gevent

greenlet

为了更好使用 协程 来完成多任务,python 中 greenlet 模块对其封装,从而使得切换任务变得更加简单

安装方式:pip3 install 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()  # 切换到 gr1 开始运行, 即 从 gr1 对应 的 test1 开始运行
# 12
# 56
# 34

当创建一个 greenlet 时,首先初始化一个空的栈, switch 到这个栈的时候,会运行在 greenlet 构造时传入的函数(首先在test1中打印 12), 如果在这个函数(test1)中 switch 到其他协程(到了test2 打印 56),那么该协程会被挂起,等到切换回来(在test2 中切换到 test1 打印34)。当这个协程对应函数执行完毕,那么这个协程就变成dead状态。

greenlet 的 module 与 class

dir(greenlet)
[‘GREENLET_USE_CONTEXT_VARS’, ‘GREENLET_USE_GC’, ‘GREENLET_USE_TRACING’, ‘GreenletExit’, ‘_C_API’, ‘doc’, ‘file’, ‘loader’, ‘name’, ‘package’, ‘spec’, ‘version’, ‘error’, ‘getcurrent’, ‘gettrace’, ‘greenlet’, ‘settrace’]

其中,比较重要的是: getcurrent(), 类greenlet、异常类GreenletExit 。
getcurrent() : 返回当前的greenlet实例;
GreenletExit: 是一个特殊的异常,当触发了这个异常的时候,即使不处理,也不会抛到其parent(后面会提到协程中对返回值或者异常的处理)
greenlet.greenlet:

[‘GreenletExit’, ‘bool’, ‘class’, ‘delattr’, ‘dict’, ‘dir’, ‘doc’, ‘eq’, ‘format’, ‘ge’, ‘getattribute’, ‘getstate’, ‘gt’, ‘hash’, ‘init’, ‘init_subclass’, ‘le’, ‘lt’, ‘ne’, ‘new’, ‘reduce’, ‘reduce_ex’, ‘repr’, ‘setattr’, ‘sizeof’, ‘str’, ‘subclasshook’, ‘_stack_saved’, ‘dead’, ‘error’, ‘getcurrent’, ‘gettrace’, ‘gr_frame’, ‘parent’, ‘run’, ‘settrace’, ‘switch’, ‘throw’]比较重要的几个属性:

  • run:当 greenlet 启动的时候会调用到这个callable,如果我们需要继承greenlet.greenlet时,需要重写该方法
  • switch:前面已经介绍过了,在greenlet之间切换
  • parent:可读写属性,后面介绍
  • dead:如果greenlet执行结束,那么该属性为true
  • throw:切换到指定greenlet后立即跑出异常
import greenlet
 
 
def test1(x, y):
    z = gr2.switch(x+y)
    print('test1 ', z)
 
 
def test2(u):
    print('test2 ', u)
    gr1.switch(10)
 
 
gr1 = greenlet.greenlet(test1)
gr2 = greenlet.greenlet(test2)
print(gr1.switch("hello", " world"))

switch not all

import greenlet
 
 
def test1(x, y):
    z = gr2.switch(x+y)
    print('test1 ', z)
 
 
def test2(u):
    print('test2 ', u)
    gr1.switch(10)
 
 
gr1 = greenlet.greenlet(test1)
gr2 = greenlet.greenlet(test2)
print(gr1.switch("hello", " world"))
# 'test2 ' 'hello world'
# 'test1 ' 10
# None

每一个 Greenlet 都有一个 parent,一个新的 greenlet 在哪里创生,当前环境的 greenlet 就是这个新 greenlet 的 parent。所有的greenlet 构成一棵树,其跟节点就是还没有手动创建 greenlet 时候的 ”main” greenlet(事实上,在首次 import greenlet 的时候实例化)。当一个协程 正常结束,执行流程回到其对应的parent;或者在一个协程中抛出未被捕获的异常,该异常也是传递到其parent。

greenlet 生命周期

from greenlet import greenlet
 
 
def test1():
    gr2.switch(1)
    print('test1 finished')
 
 
def test2(x):
    print('test2 first', x)
    z = gr1.switch()
    print('test2 back', z)
 
 
gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch()
print('gr1 is dead?: %s, gr2 is dead?: %s' % (gr1.dead, gr2.dead))
gr2.switch()
print('gr1 is dead?: %s, gr2 is dead?: %s' % (gr1.dead, gr2.dead))
print(gr2.switch(10))

test2 first 1
test1 finished
gr1 is dead?: True, gr2 is dead?: False
test2 back ()
gr1 is dead?: True, gr2 is dead?: True
10

从这个例子可以看出:
1.只有当协程对应的函数执行完毕,协程才会 die,所以第一次 Check 的时候 gr2 并没有 die,因为第 9 行切换出去了就没切回来。在 main 中再 switch 到 gr2 的时候, 执行后面的逻辑,gr2 die
2.如果试图再次 switch 到一个已经是 dead 状态的 greenlet 会怎么样呢,事实上会切换到其 parent greenlet。

Greenlet Traceing

Greenlet 也提供了接口使得程序员可以监控 greenlet 的整个调度流程。主要是 gettrace 和 settrace(callback) 函数。

import greenlet
 
 
def test_greenlet_tracing():
    def callback(event, args):
        print(event, 'from', id(args[0]), 'to', id(args[1]))
 
    def dummy():
        g2.switch()
 
    def dummyexception():
        raise Exception('excep in coroutine')
 
    main = greenlet.getcurrent()
    g1 = greenlet.greenlet(dummy)
    g2 = greenlet.greenlet(dummyexception)
    print('main id %s, gr1 id %s, gr2 id %s' % (id(main), id(g1), id(g2)))
    oldtrace = greenlet.settrace(callback)
    try:
        g1.switch()
    except BaseException as e:
        print('Exception : ', e)
    finally:
        greenlet.settrace(oldtrace)
 
 
test_greenlet_tracing()

main id 1397838280136, gr1 id 1397838280312, gr2 id 1397838280488
switch from 1397838280136 to 1397838280312
switch from 1397838280312 to 1397838280488
throw from 1397838280488 to 1397838280136
Exception : excep in coroutine

其中 callback 函数 event 是 switch 或者 throw 之一,表明是正常调度还是异常跑出;args 是二元组,表示是从协程 args[0] 切换到了协程 args[1]。上面的输出展示了切换流程:从 main 到 gr1,然后到 gr2,最后回到 main。

greenlet使用建议

使用greenlet需要注意一下三点:

  1. greenlet 创建之后,一定要结束,不能 switch 出去就不回来了,否则容易造成内存泄露
  2. python 中每个线程都有自己的 main greenlet 及其对应的 sub-greenlet ,不同线程之间的 greenlet 是不能相互切换的
  3. 不能存在循环引用,这个是官方文档明确说明:”Greenlets do not participate in garbage collection; cycles involving data that is present in a greenlet’s frames will not be detected. “
from greenlet import greenlet, GreenletExit
 
huge = []
 
 
def show_leak():
    def test1():
        gr2.switch()
 
    def test2():
        huge.extend([x * x for x in range(100)])
        gr1.switch()
        print('finish switch del huge')
        del huge[:]
 
    gr1 = greenlet(test1)
    gr2 = greenlet(test2)
    gr1.switch()
    gr1 = gr2 = None
    print('length of huge is zero ? %s' % len(huge))
 
 
if __name__ == '__main__':
    show_leak()
 
# output: length of huge is zero ? 100

在test2函数中,第11行,我们将huge清空,然后再第16行将gr1、gr2的引用计数降到了0。但运行结果告诉我们,第11行并没有执行,所以如果一个协程没有正常结束是很危险的,往往不符合程序员的预期。greenlet提供了解决这个问题的办法,官网文档提到:如果一个greenlet实例的引用计数变成0,那么会在上次挂起的地方抛出GreenletExit异常,这就使得我们可以通过try … finally 处理资源泄露的情况。如下面的代码:


from greenlet import greenlet, GreenletExit
 
huge = []
 
 
def show_leak():
    def test1():
        gr2.switch()
 
    def test2():
        huge.extend([x * x for x in range(100)])
        try:
            gr1.switch()
        finally:
            print('finish switch del huge')
            del huge[:]
 
    gr1 = greenlet(test1)
    gr2 = greenlet(test2)
    gr1.switch()
    gr1 = gr2 = None
    print('length of huge is zero ? %s' % len(huge))
 
 
if __name__ == '__main__':
    show_leak()
 
 
# output :
# finish switch del huge

上述代码的switch流程:main greenlet --> gr1 --> gr2 --> gr1 --> main greenlet, 很明显gr2没有正常结束(在第10行挂起了)。第18行之后gr1,gr2的引用计数都变成0,那么会在第10行抛出GreenletExit异常,因此finally语句有机会执行。同时,在文章开始介绍Greenlet module的时候也提到了,GreenletExit这个异常并不会抛出到parent,所以main greenlet也不会出异常。

看上去貌似解决了问题,但这对程序员要求太高了,百密一疏。所以最好的办法还是保证协程的正常结束。

gevent

安装:pip3 install gevent 

因为 python 线程的性能问题,在python中使用多线程运行代码经常不能达到预期的效果。而有些时候我们的逻辑中又需要开更高的并发,或者简单的说,就是让我们的代码跑的更快,在同样时间内执行更多的有效逻辑、减少无用的等待。gevent 就是一个现在很火、支持也很全面的 python第三方协程库。

Python 通过 yield 提供了对 协程 的基本支持,但是不完全。greenlet 已经实现了协程,但是这个需要工人切换,有点麻烦。python 还有一个比 greenlet 更强大的并且能够自动切换任务的模块 gevent,gevent 为 Python提供了比较完善的协程支持。

gevent 是 Python 的第三方并发框架库,以微线程greenlet为核心,使用了epoll事件监听机制以及诸多其他优化而变得高效。而且其中有个monkey类,将现有基于Python 线程直接转化为 greenlet(类似于打patch),通过 greenlet 实现协程,其基本思想是:当一个 greenlet 遇到IO操作时,比如访问网络,就自动切换到其他的 greenlet,等到 IO 操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了 gevent 为我们自动切换协程,就保证总有 greenlet 在运行,而不是等待IO。同时也因为只有一个线程在执行,会极大的减少上下文切换的成本。

gevent使用

from gevent import monkey; monkey.patch_socket()
import gevent
 
 
def f(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
 
 
g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()
# -*- coding: utf-8 -*-
 
from gevent import monkey; monkey.patch_all()
import gevent
import requests
from datetime import datetime
 
 
def f(url):
    print(f'time: {datetime.now()}, GET: {url}')
    resp = requests.get(url)
    print(f'time: {datetime.now()}, {len(resp.text)} bytes received from {url}.')
 
 
gevent.joinall([
        gevent.spawn(f, 'https://www.python.org/'),
        gevent.spawn(f, 'https://www.yahoo.com/'),
        gevent.spawn(f, 'https://github.com/'),
])

加锁

# -*- coding: utf-8 -*-
 
import gevent
from gevent.lock import Semaphore
 
sem = Semaphore(1)
 
 
def f1():
    for i in range(5):
        sem.acquire()
        print('run f1, this is ', i)
        sem.release()
        gevent.sleep(1)
 
 
def f2():
    for i in range(5):
        sem.acquire()
        print('run f2, that is ', i)
        sem.release()
        gevent.sleep(0.3)
 
 
t1 = gevent.spawn(f1)
t2 = gevent.spawn(f2)
gevent.joinall([t1, t2])

gevent 调度过程解析

https://www.jianshu.com/p/f55148c41f54

gevent 比较重要的模块

https://www.jianshu.com/p/0a0feb3fe361

gevent 比较重要的模块,包括Timeout,Event/AsynResult,Semphore,socket patch,这些模块都涉及当前协程与 hub 的切换。

 类似资料: