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

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

本文详解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 相关产品和应用

全部下载

专题列表