直接用gevent.spawn会失效,根本原因是未打补丁或未启动事件循环;必须在程序最开头执行gevent.monkey.patch_all(),否则io仍阻塞,协程无法切换。

为什么直接用 gevent.spawn 会失效?
很多人一上来就写 gevent.spawn(func),发现协程没跑、任务串行、甚至报 LoopExit 错误——根本原因是没打补丁或没启动事件循环。Gevent 不是“开箱即用”的并发库,它依赖 monkey patch 替换标准库的阻塞调用(比如 socket、ssl、threading),否则所有 IO 操作仍走原生阻塞路径,协程根本切不走。
实操建议:
- 必须在程序最开头(import 任何可能触发 IO 的模块之前)执行
gevent.monkey.patch_all() - 如果用了
requests,要确保 patch 包含urllib3(默认已包含,但自定义 urllib3 版本时需留意) - 避免混用
threading或subprocess:这些未被 patch 的模块会阻塞整个 event loop - 调试时加
GEVENT_TRACE=1环境变量,可看到协程切换日志
gevent.joinall 和 pool 哪个更适合批量 HTTP 请求?
两者都能等协程结束,但语义和控制粒度不同:gevent.joinall 是简单等待一组 greenlet;gevent.pool.Pool 提供并发数限制和复用能力,对 IO 密集型任务更实用。
实操建议:
- 无限制并发(如
joinall([spawn(f) for f in tasks]))容易触发目标服务限流或本地文件描述符耗尽,尤其在高 QPS 场景下 - 用
Pool(size=20)控制最大并发数,比手动计数或信号量更可靠 - Pool 的
spawn方法返回 greenlet,可配合greenlet.get()获取结果或捕获异常 - 注意:Pool 不自动清理 dead greenlet,长期运行服务建议定期调用
pool.free_count()监控
如何让 requests 在 gevent 下真正异步?
标准 requests 底层用的是阻塞式 urllib3,即使打了 monkey patch,某些场景(如重定向多跳、连接池复用)仍可能退化为同步行为。这不是 bug,而是 patch 的局限性。
实操建议:
- 确认 patch 顺序:必须在
import requests之前执行gevent.monkey.patch_all() - 禁用重定向(
allow_redirects=False)或手动处理跳转,减少 urllib3 内部同步等待 - 设置短超时:
timeout=(3, 3),避免单个请求拖垮整组协程 - 更稳妥的做法是换用
grequests(基于gevent封装的 requests),或直接上httpx+asyncio(但那就不是 gevent 了)
协程崩溃后怎么不中断整个程序?
一个 greenlet 抛出未捕获异常,默认会终止自身,但不会影响其他 greenlet;问题是,如果主流程用 joinall 等待全部完成,而其中某个失败了,joinall 会把异常原样抛出,导致整个批处理中断。
实操建议:
- 每个 spawn 的函数内部加
try/except,把异常吞掉或记录到日志,再返回错误标记 - 用
gevent.spawn(lambda: func()).link_exception(handler)单独监听异常 - 更推荐:用
pool.imap_unordered替代joinall,它返回迭代器,失败项会被跳过,不影响后续结果 - 别依赖
greenlet.exc_info做全局错误处理——它只在 greenlet 内部有效,且易被覆盖
真正的难点不在语法,而在理解 gevent 的“伪线程”模型:它共享主线程的上下文、没有真正的调度优先级、异常传播路径和线程完全不同。一旦 mix-in 了 threading 或 asyncio,基本就等于放弃 gevent 的一致性保证。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











