Python并发编程协程(Coroutine)之Gevent详解

Felix
Felix
发布时间:2026-10-13 | 2人浏览过
原创

本文深入解析Python中的Gevent库,阐述协程与进程、线程的区别,详解Greenlet的工作机制及Gevent的猴子补丁特性。通过对比代码示例,揭示Gevent队列在IO调度中的潜在问题,并探讨其局限性及asyncio的替代优势,帮助开发者掌握高效异步I/O编程技巧。

Gevent官方文档地址:http://www.gevent.org/contents.html

基本概念

通常所说的协程(Coroutine)一词,实为coroutine的缩写,直译为协同例程,业内常简称为协程。

在Linux系统中,线程被视为轻量级进程,而协程则被进一步定义为轻量级线程,即微线程。

进程和协程

以下对比进程与协程的异同点:

相同点:

两者均可视为执行流,支持挂起操作,并能在挂起点恢复执行。这本质上属于continuation(延续)的概念,我们可以通过在Linux上运行hello程序来理解这一机制:

shell进程与hello进程的交互过程:

初始状态下,shell进程处于运行状态,等待用户输入命令行指令。

当执行hello程序时,shell通过系统调用发出请求,此时系统调用将控制权移交给操作系统。操作系统会保存shell进程的上下文,创建hello进程及其上下文,并将控制权转移给新的hello进程。

hello进程执行完毕后,操作系统恢复shell进程的上下文,并将控制权交还给shell进程。

随后,shell进程继续等待下一条命令的输入。

当挂起一个执行流时,需要保存以下关键信息:

栈空间:切换前需保存局部变量及函数调用栈,否则无法恢复执行状态。

寄存器状态:用于记录执行流恢复后需执行的操作。

寄存器与栈的结合构成了上下文,理解上下文切换机制如下:

CPU看似并发执行多个进程,实则是通过处理器在进程间快速切换实现的。操作系统实现这种交错执行的机制称为上下文切换。

操作系统需跟踪进程运行所需的所有状态信息,这些状态统称为上下文。

在任一时刻,操作系统仅能执行一个进程代码。当决定将控制权从当前进程转移至新进程时,需进行上下文切换:保存当前进程上下文,恢复新进程上下文,并将控制权移交新进程,使其从上次停止处继续执行。

不同点:

调度者不同:进程由内核调度,协程由用户态调度。这意味着进程的上下文保存与恢复在内核态进行,而协程在用户态进行,显然用户态的开销更低。

抢占机制不同:进程会被强占,而协程不会。若协程不主动让出CPU,其他协程将无法获得执行机会。

内存占用不同:协程仅需约4K栈空间即可运行,而进程占用的内存资源要大得多。

从操作系统视角看,多协程程序表现为单进程、单协程模型。

线程和协程

鉴于协程也被称为微线程,以下对比协程与线程的差异:

线程间的上下文切换成本相对较高,尤其在开启大量线程时;而协程的切换成本极低。

线程切换主要由操作系统控制,而协程的执行由开发者自主控制。

通过下图可更直观地理解:

从上图可见,协程仅在单一线程内不同协程间切换,这与线程在进程内不同线程间切换类似,这也可能是协程被称为微线程的原因。

进一步分析协程:

Gevent

Gevent是一个基于协程的Python网络库,利用Greenlet提供的、封装了libevent事件循环的高层同步API。它使开发者无需改变编程习惯,即可用同步方式编写异步I/O代码。

使用Gevent的性能通常高于传统线程,甚至高出许多。但需注意其一个潜在问题:

猴子补丁(Monkey-patching):若启用此补丁,Gevent会直接修改标准库中大部分阻塞式系统调用(包括socket、ssl、threading和select等模块),使其变为协作式运行。然而,在复杂的生产环境中,无法保证所有使用这些标准库的地方不会因打补丁而出现异常问题。

第三方库支持:需确保项目中使用的其他网络库为纯Python实现,或明确说明支持Gevent。

既然Gevent基于Greenlet,通过下图理解Greenlet机制:

每个协程都有一个父协程,最顶层协程为主线程或当前线程。每个协程遇到IO时,将控制权交回最顶层协程;顶层协程检查哪个协程的IO事件已完成,便将控制权交还给该协程。

以下是Greenlet的一个示例:

from greenlet import greenlet

def test1(x,y):
  z = gr2.switch(x+y)
  print(z)

def test2(u):
  print(u)
  gr1.switch(42)

gr1 = greenlet(test1)
gr2 = greenlet(test2)

gr1.switch("hello",'world')

greenlet(run=None, parent=None):创建一个greenlet实例。 gr.parent:每个协程都有一个父协程,当前协程结束后会回到父协程中执行,该属性默认是创建该协程的协程。 gr.run:该属性是协程实际运行的代码。run方法结束了,那么该协程也就结束了。 gr.switch(*args, **kwargs):切换到gr协程。 gr.throw():切换到gr协程,接着抛出一个异常。

以下是Gevent的一个示例:

import gevent
def func1():
  print("start func1")
  gevent.sleep(1)
  print("end func1")
def func2():
  print("start func2")
  gevent.sleep(1)
  print("end func2")

gevent.joinall(
  [
    gevent.spawn(func1),
    gevent.spawn(func2)
  ]
)

关于Gevent中队列的使用

Gevent内置了队列,但在实际使用中我发现一个问题:若在协程中通过队列q传递数据,当队列为空时,从队列获取数据的那个协程会被切换到另一个用于往队列put数据的协程。问题在于,Gevent不认为放入数据为IO操作,不会立即切换到上一个协程,而是先完成当前协程的任务再切换。我原本期望的效果是:往队列放入数据后,立即切换到get的那个协程。(或许我的理解有误)以下是测试代码:

import gevent
from gevent.queue import Queue
def func():
  for i in range(10):

    print("int the func")
    q.put("test")
def func2():
  for i in range(10):
    print("int the func2")
    res = q.get()
    print("--->",res)
q = Queue()
gevent.joinall(
  [
    gevent.spawn(func2),
    gevent.spawn(func),
  ]
)

这段代码的运行效果为:

若在fun函数的q.put("test")后添加gevent.sleep(0),则效果如下:

原本预测在不修改代码的情况下应得到第二个图的结果,但实际却是第一个图的结果(此问题可能是我自己未深入研究透彻,后续将继续探究)。

关于Gevent的问题

正如前述,Gevent与第三方库配合使用时可能存在一些问题,总结如下: Python协程库可直接进行monkey patch。 C语言编写的库可采用豆瓣开源的greenify进行patch(此功能计划后续进行测试)。

总体而言,Gevent目前仍存在诸多缺陷,且非官方标准库。在Python 3中,有一个官方正在开发并于3.6版本中稳定的库asyncio,这是一个极具野心的库,非常建议学习,我也计划后续深入探究。

总结

以上就是本文关于Python并发编程协程(Coroutine)之Gevent详解的全部内容,希望对大家有所帮助。感兴趣的朋友可以继续参阅本站其他相关专题,如有不足之处,欢迎留言指出。感谢朋友们对本站的支持!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表