杰瑞科技汇

Python gevent多线程如何高效实现并发?

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

Python gevent多线程如何高效实现并发?-图1
(图片来源网络,侵删)

核心概念: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 的核心魔法在于 猴子补丁

Python gevent多线程如何高效实现并发?-图2
(图片来源网络,侵删)
  1. 事件循环gevent 内部有一个事件循环,它会持续检查哪些协程已经准备好执行(网络数据已经到达)。
  2. 自动切换:当一个 gevent 协程执行到一个 I/O 阻塞 操作时(socket.connect(), requests.get(), time.sleep()),gevent 会自动暂停这个协程的执行,并将其挂起。
  3. 切换到下一个:事件循环会立即切换到另一个已经准备好的协程,让它继续运行。
  4. 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 秒。
主程序结束。

分析

Python gevent多线程如何高效实现并发?-图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 与多线程的混合使用

geventthreading 是可以结合使用的,但这通常发生在比较复杂的场景下。

场景:你有一个多线程的 Web 服务器框架(Flask/Django),其中某个线程需要执行一个 I/O 密集型的任务(比如调用多个外部 API),为了避免阻塞这个工作线程,你可以在这个线程内部使用 gevent 来并发执行这些 API 调用。

重要提示geventmonkey.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 扩展库交互 threadingmultiprocessing 一些 C 扩展库可能有自己的锁机制,与 gevent 的协作式模型不兼容,容易导致死锁。
需要利用多核 CPU multiprocessing gevent 仍然是单进程单线程模型(尽管有多个 greenlet),无法利用多核 CPU。multiprocessing 可以。
简单、快速的并发 concurrent.futures.ThreadPoolExecutor 对于少量并发任务,标准库的线程池使用起来更简单直观,无需额外安装 gevent

一句话概括

gevent 处理网络和磁盘 I/O,用 multiprocessing 处理 CPU 计算。

分享:
扫描分享到社交APP
上一篇
下一篇