这是一个非常重要且常见的概念,因为 gevent 本身并不是传统的多线程模型,但它提供了一种比多线程更轻量、更高效的并发编程方式。

核心概念:gevent 是什么?
gevent 是一个基于 协程 的 Python 网络库,它通过一个名为 greenlet 的微线程(协程)实现,并结合一个 事件循环(通常是 libev),提供了极高的并发性能。
你可以把 gevent 理解为 “用户态的、协作式的多线程”。
gevent vs. 传统多线程
为了理解 gevent,我们首先要明白它和 Python 的标准库 threading(多线程)的区别。
| 特性 | gevent (协程) |
threading (多线程) |
|---|---|---|
| 执行单元 | Greenlet (微线程,用户态) | Thread (操作系统线程,内核态) |
| 切换方式 | 协作式 (遇到 I/O 操作自动切换) | 抢占式 (操作系统调度,时间片轮转) |
| 内存开销 | 极小 (一个 greenlet 只占用几 KB) | 较大 (一个线程默认占用几 MB) |
| 创建/销毁成本 | 极低 (可以轻松创建成千上万个) | 较高 (创建和销毁开销大) |
| GIL 影响 | 规避 GIL (同一时间只有一个 greenlet 在运行,但切换快) | 受 GIL 限制 (同一时间只有一个线程执行 Python 字节码) |
| 适用场景 | I/O 密集型 (网络请求、磁盘读写、数据库查询) | CPU 密集型 (大量计算、科学计算) |
gevent 的工作原理:猴子补丁
gevent 的核心魔法在于 猴子补丁。

- 事件循环:
gevent内部有一个事件循环,它会持续检查哪些协程已经准备好执行(网络数据已经到达)。 - 自动切换:当一个
gevent协程执行到一个 I/O 阻塞 操作时(socket.connect(),requests.get(),time.sleep()),gevent会自动暂停这个协程的执行,并将其挂起。 - 切换到下一个:事件循环会立即切换到另一个已经准备好的协程,让它继续运行。
monkey.patch_all():为了让 Python 的标准库(如socket,ssl,threading,time等)能够触发这种自动切换,gevent提供了monkey.patch_all(),这个函数会“打补丁”,修改这些标准库的底层实现,使其在遇到 I/O 阻塞时,不是真的阻塞,而是触发协程的切换。
关键点:gevent 的切换只发生在 I/O 操作上,如果一个协程在进行纯 CPU 计算,它会一直运行下去,直到计算完成,不会主动出让 CPU。
gevent 编程实践
基本使用
我们从一个简单的例子开始,模拟 5 个协程并发执行任务。
import gevent
from gevent import monkey
# 必须在导入其他可能涉及 I/O 的库之前打补丁
monkey.patch_all()
def task(name, seconds):
print(f"协程 {name} 开始执行...")
# gevent.sleep() 是一个会触发协程切换的 I/O 操作
gevent.sleep(seconds)
print(f"协程 {name} 执行完毕,耗时 {seconds} 秒。")
if __name__ == '__main__':
print("主程序开始,创建协程...")
# 创建 5 个协程对象
greenlets = [
gevent.spawn(task, "A", 2),
gevent.spawn(task, "B", 1),
gevent.spawn(task, "C", 3),
gevent.spawn(task, "D", 1.5),
gevent.spawn(task, "E", 0.5)
]
# gevent.joinall 会阻塞,直到所有 greenlets 执行完毕
gevent.joinall(greenlets)
print("主程序结束。")
输出结果:
主程序开始,创建协程...
协程 A 开始执行...
协程 B 开始执行...
协程 C 开始执行...
协程 D 开始执行...
协程 E 开始执行...
协程 E 执行完毕,耗时 0.5 秒。
协程 B 执行完毕,耗时 1 秒。
协程 D 执行完毕,耗时 1.5 秒。
协程 A 执行完毕,耗时 2 秒。
协程 C 执行完毕,耗时 3 秒。
主程序结束。
分析:

- 主程序创建了 5 个协程,它们几乎是同时启动的。
gevent.spawn只是创建协程对象,并不会立即执行。gevent.joinall会启动事件循环,开始调度这些协程。- 当
task函数执行到gevent.sleep()时,协程会“休眠”,gevent会切换到其他正在等待的协程运行。 - 我们可以看到,协程 E (耗时 0.5s) 最先完成,然后是 B (1s),依此类推,总耗时大约是 3 秒(最慢的那个协程的耗时),而不是累加的 8 秒,体现了并发优势。
结合 requests 进行网络请求
这是 gevent 最经典的应用场景:并发爬取网页。
import gevent
from gevent import monkey
import requests
# 打补丁,让 requests 库也能触发协程切换
monkey.patch_all()
def fetch_url(url):
try:
print(f"正在请求: {url}")
# requests.get() 是一个 I/O 密集型操作
response = requests.get(url, timeout=5)
print(f"成功请求: {url}, 状态码: {response.status_code}")
except Exception as e:
print(f"请求 {url} 失败: {e}")
if __name__ == '__main__':
urls = [
'https://www.python.org',
'https://github.com',
'https://www.baidu.com',
'https://www.qq.com'
]
print("开始并发请求...")
# 创建并启动所有协程
greenlets = [gevent.spawn(fetch_url, url) for url in urls]
# 等待所有协程完成
gevent.joinall(greenlets)
print("所有请求完成。")
在这个例子中,当第一个 requests.get() 发起请求并等待服务器返回数据时,gevent 会自动切换到下一个协程,发起第二个请求,而不是傻傻地等待,这大大提高了网络请求的效率。
gevent 与多线程的混合使用
gevent 和 threading 是可以结合使用的,但这通常发生在比较复杂的场景下。
场景:你有一个多线程的 Web 服务器框架(Flask/Django),其中某个线程需要执行一个 I/O 密集型的任务(比如调用多个外部 API),为了避免阻塞这个工作线程,你可以在这个线程内部使用 gevent 来并发执行这些 API 调用。
重要提示:gevent 的 monkey.patch_all() 不能 在多线程环境中使用,否则会破坏 threading 模块的工作方式,正确的做法是,只在需要用到 gevent 的那个线程内部进行补丁和应用。
示例:一个线程使用 gevent 并发执行任务。
import threading
import gevent
from gevent import monkey
import time
def run_in_gevent():
print("在 gevent 线程中开始执行...")
# 只在这个线程内部打补丁
monkey.patch_all()
def gevent_task(n):
print(f" Gevent 协程 {n} 开始")
gevent.sleep(1)
print(f" Gevent 协程 {n} 结束")
# 创建并运行多个 gevent 协程
greenlets = [gevent.spawn(gevent_task, i) for i in range(3)]
gevent.joinall(greenlets)
print("在 gevent 线程中执行结束。")
def run_in_thread():
print("在普通线程中开始执行...")
time.sleep(2)
print("在普通线程中执行结束。")
if __name__ == '__main__':
# 创建两个线程
t1 = threading.Thread(target=run_in_gevent)
t2 = threading.Thread(target=run_in_thread)
t1.start()
t2.start()
t1.join()
t2.join()
print("主程序结束。")
总结与何时选择
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| I/O 密集型任务 (网络请求、文件读写、数据库交互) | gevent |
轻量级,切换开销小,能轻松处理成千上万的并发连接,有效规避 GIL。 |
| CPU 密集型任务 (数值计算、图像处理、数据分析) | multiprocessing (多进程) |
Python 的 GIL 会限制多线程的 CPU 利用率,多进程可以绕过 GIL,充分利用多核 CPU。 |
| 需要与 Python 的 C 扩展库交互 | threading 或 multiprocessing |
一些 C 扩展库可能有自己的锁机制,与 gevent 的协作式模型不兼容,容易导致死锁。 |
| 需要利用多核 CPU | multiprocessing |
gevent 仍然是单进程单线程模型(尽管有多个 greenlet),无法利用多核 CPU。multiprocessing 可以。 |
| 简单、快速的并发 | concurrent.futures.ThreadPoolExecutor |
对于少量并发任务,标准库的线程池使用起来更简单直观,无需额外安装 gevent。 |
一句话概括:
用
gevent处理网络和磁盘 I/O,用multiprocessing处理 CPU 计算。
