Python gevent协程:原理、猴子补丁与异步实践

认真发呆
认真发呆
发布时间:2026-10-14| 7人浏览过
原创

本文详解Python中gevent模块的协程机制。通过对比标准线程与协程,阐述gevent如何利用greenlet和epoll实现高效IO切换。重点解析time.sleep等阻塞操作无法被自动捕获的问题,并介绍通过猴子补丁(monkey.patch_all)实现异步高并发的解决方案。最后结合requests异步请求与信号量锁的使用,提供完整的实战代码示例。

背景

由于Python全局解释器锁(GIL)的限制,多线程在CPU密集型任务中性能受限,往往难以达到预期的并发效果。而在实际业务场景中,高并发IO操作需求日益增长,这就要求程序能在单位时间内执行更多有效逻辑,同时最小化因等待IO产生的无用耗时。

什么是协程?

如果将线程视为轻量级进程,那么协程可被理解为轻量级线程。协程允许在单个线程中执行函数A时,随时中断并切换到函数B执行,且切换过程无需像多线程那样进行上下文切换。尽管现象上类似多线程,但其本质仍运行在单一线程内。gevent是Python生态中实现协程的经典库。

什么是 gevent?

gevent是一个基于greenlet库构建的Python并发框架,底层利用epoll事件监听机制及多项优化技术提升效率。其核心逻辑是:每个greenlet实例代表一个协程。当greenlet执行到IO操作(如网络请求)时,gevent会自动将其挂起并切换到其他就绪的greenlet;待IO完成后,再切回原协程继续执行。这一切换过程由gevent自动处理,无需开发者手动干预。

协程的例子

代码:


import gevent,time

def f1():
    for i in range(5):
        print('function:@@@f1 | NUM: @@@',i)

        # 此处阻塞,gevent会帮我们切换到其他协程去↓
        gevent.sleep(0)

def f2():
    for i in range(5):
        print('function:@@@f2 | NUM: @@@',i)

        # 此处阻塞,gevent会帮我们切换到其他协程去↑
        gevent.sleep(0)

# 创建两个协程对象,分别去执行两个函数
xc1=gevent.spawn(f1)
xc2=gevent.spawn(f2)

# 将协程们交给gevent去执行
gevent.joinall([xc1,xc2])

执行结果:

function:@@@f1 | NUM: @@@ 0 function:@@@f2 | NUM: @@@ 0 function:@@@f1 | NUM: @@@ 1 function:@@@f2 | NUM: @@@ 1 function:@@@f1 | NUM: @@@ 2 function:@@@f2 | NUM: @@@ 2 function:@@@f1 | NUM: @@@ 3 function:@@@f2 | NUM: @@@ 3 function:@@@f1 | NUM: @@@ 4 function:@@@f2 | NUM: @@@ 4

上述示例展示了gevent对协程的调度机制。当协程xc1执行至gevent.sleep(0)时发生阻塞,gevent随即切换至xc2;当xc2执行至相同的阻塞点时,gevent再次切换回xc1。这种交替执行机制确保了在IO等待期间,其他协程得以运行,从而提升整体效率。

Q&A

Q:gevent 无法捕获的耗时

代码:


import gevent,time

def f1():
    for i in range(5):
        print('function:@@@f1 | NUM: @@@',i)

        # 注意这里
        time.sleep(0.1)

def f2():
    for i in range(5):
        print('function:@@@f2 | NUM: @@@',i)

        # 注意这里
        time.sleep(0.1)

# 创建两个协程对象,分别去执行两个函数
xc1=gevent.spawn(f1)
xc2=gevent.spawn(f2)

# 将协程们交给gevent去执行
gevent.joinall([xc1,xc2])

执行结果:

function:@@@f1 | NUM: @@@ 0 function:@@@f1 | NUM: @@@ 1 function:@@@f1 | NUM: @@@ 2 function:@@@f1 | NUM: @@@ 3 function:@@@f1 | NUM: @@@ 4 function:@@@f2 | NUM: @@@ 0 function:@@@f2 | NUM: @@@ 1 function:@@@f2 | NUM: @@@ 2 function:@@@f2 | NUM: @@@ 3 function:@@@f2 | NUM: @@@ 4

观察上述输出可知,当代码中使用标准库time.sleep(0.1)时,gevent无法识别该阻塞行为,导致协程xc1和xc2串行执行,未能发挥异步并发的优势。尽管创建了协程对象,但实际效果等同于同步代码。为解决此问题,需引入特定解决方案。

A:猴子补丁

针对标准库中gevent无法自动捕获的耗时操作,gevent提供了“猴子补丁”(Monkey Patching)机制。通过应用猴子补丁,程序在遇到任何IO耗时操作时,gevent均能自动识别并触发协程切换,从而实现真正的异步高并发处理。

代码:


import gevent,time
from gevent import monkey;monkey.patch_all()

def f1():
    for i in range(5):
        print('function:@@@f1 | NUM: @@@',i)

        # 注意这里
        time.sleep(0.1)

def f2():
    for i in range(5):
        print('function:@@@f2 | NUM: @@@',i)

        # 注意这里
        time.sleep(0.1)

# 创建两个协程对象,分别去执行两个函数
xc1=gevent.spawn(f1)
xc2=gevent.spawn(f2)

# 将协程们交给gevent去执行
gevent.joinall([xc1,xc2])

执行结果:

function:@@@f1 | NUM: @@@ 0 function:@@@f2 | NUM: @@@ 0 function:@@@f1 | NUM: @@@ 1 function:@@@f2 | NUM: @@@ 1 function:@@@f1 | NUM: @@@ 2 function:@@@f2 | NUM: @@@ 2 function:@@@f1 | NUM: @@@ 3 function:@@@f2 | NUM: @@@ 3 function:@@@f1 | NUM: @@@ 4 function:@@@f2 | NUM: @@@ 4

应用猴子补丁后,输出显示协程切换已恢复正常,f1与f2交替执行,问题得到解决。

实践

异步 requests 请求

代码:


from gevent import monkey;monkey.patch_all()
import gevent,time,requests
from urllib3 import disable_warnings

disable_warnings()

def req(url):
    res = requests.get(url,verify=False)
    if res:
        print('URL:{} | CODE:{}!'.format(url,res.status_code))
    else:
        print('URL:{} FAILED!')

xc1=gevent.spawn(req,'https://www.baidu.com')
xc2=gevent.spawn(req,'https://www.gitee.com')
xc3=gevent.spawn(req,'https://www.huaweicloud.com')

gevent.joinall([xc1,xc2,xc3])

执行结果:

URL:https://www.baidu.com | CODE:200! URL:https://www.huaweicloud.com | CODE:200! URL:https://www.gitee.com | CODE:200!

gevent 的锁

代码:


from gevent import monkey;monkey.patch_all()
from gevent.lock import Semaphore
import gevent,time

# 信号量设置为1
s1=Semaphore(1)

def f1():
    for i in range(5):
        # 信号量-1,即拿到锁
        s1.acquire()
        print('function:@@@f1 | NUM: @@@',i)

        # 信号量+1,即释放锁
        s1.release()

        # 猴子补丁帮忙识别阻塞
        time.sleep(0.1)

def f2():
    for i in range(5):
        # 信号量-1,即拿到锁
        s1.acquire()
        print('function:@@@f2 | NUM: @@@',i)

        # 信号量+1,即释放锁
        s1.release()

        # 猴子补丁帮忙识别阻塞
        time.sleep(0.3)

# 创建两个协程对象,分别去执行两个函数
xc1=gevent.spawn(f1)
xc2=gevent.spawn(f2)

# 将协程们交给gevent去执行
gevent.joinall([xc1,xc2])

执行结果:

function:@@@f1 | NUM: @@@ 0 function:@@@f2 | NUM: @@@ 0 function:@@@f1 | NUM: @@@ 1 function:@@@f1 | NUM: @@@ 2 function:@@@f2 | NUM: @@@ 1 function:@@@f1 | NUM: @@@ 3 function:@@@f1 | NUM: @@@ 4 function:@@@f2 | NUM: @@@ 2 function:@@@f2 | NUM: @@@ 3 function:@@@f2 | NUM: @@@ 4

从输出可见,gevent能够自动处理锁与阻塞的交互。若无阻塞,f1和f2应交替执行;但由于f2的阻塞时间(0.3秒)是f1(0.1秒)的三倍,前6条打印记录中f1的执行次数约为f2的三倍。这表明gevent能智能判断并处理阻塞与锁共存的情况,合理调度协程。

Tip

在HTTP异步开发中,务必在导入gevent之前应用猴子补丁(即先执行monkey.patch_all()再import gevent),否则可能导致异常或功能失效。

本文介绍了Python中gevent模块的核心用法,涵盖协程原理、猴子补丁机制及异步IO实践。如需了解更多Python gevent相关内容,建议查阅本站过往文章或关注后续更新。感谢支持!

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。