Python GIL、多进程与 asyncio
更新时间:2026-08-29。本文回答:为什么 Python 多线程跑不满多核?CPU 密集和 IO 密集分别该用什么?asyncio 和 epoll 是什么关系?
一、GIL:一把保护解释器的锁
GIL(Global Interpreter Lock,全局解释器锁)是 CPython 的一条规则:同一时刻只有一个线程能执行 Python 字节码。它的初衷是保护引用计数这类内部状态不被并发破坏。
| 场景 | 多线程效果 | 原因 |
|---|---|---|
| CPU 密集(算数、循环) | 不能并行,甚至更慢 | 线程争抢 GIL,频繁切换 |
| IO 密集(网络、磁盘) | 能"并发" | 等待 IO 时释放 GIL,别的线程接着跑 |
核心结论:GIL 限制的是"并行计算",不限制"并发等待"。IO 等待期间 GIL 被释放,所以多线程在 IO 场景仍有价值。
二、CPU 密集:用多进程绕开 GIL
要把 8 核用满做计算,正确姿势是 multiprocessing——每个进程有独立解释器和 GIL:
python
from multiprocessing import Pool
def square(x): return x * x
if __name__ == "__main__":
with Pool(8) as p: # 8 个独立进程,各自一个 GIL
print(p.map(square, range(10)))- 进程间不共享内存,靠
Queue/Pipe/shared_memory通信(类似 L2 进程模型)。 - 代价:进程创建/通信开销比线程大,数据要序列化跨进程传递。
三、IO 密集:用 asyncio 事件循环
对于海量网络连接、HTTP 请求这类 IO 等待,协程比线程更轻。asyncio 用单线程事件循环 + 协程实现并发:
python
import asyncio, aiohttp
async def fetch(url):
async with aiohttp.ClientSession() as s:
async with s.get(url) as r:
return await r.text() # 等待 IO 时让出,不阻塞线程
async def main():
urls = ["https://a", "https://b", "https://c"]
return await asyncio.gather(*(fetch(u) for u in urls))- asyncio 底层在 Linux 用
epoll做多路复用——和本站 epoll 网络低延迟 讲的是同一回事。 - 协程在单线程里切换,没有 GIL 争抢,也没有线程上下文切换开销,非常适合高并发 IO。
四、三种模型怎么选
| 任务类型 | 推荐模型 | 理由 |
|---|---|---|
| CPU 密集 | multiprocessing | 绕开 GIL,用满多核 |
| IO 密集、高并发 | asyncio | 单线程事件循环,开销最小 |
| IO 密集、逻辑简单 | 多线程 | 代码直观,IO 期间释放 GIL |
| 极性能 | C 扩展 / numpy | 热点下沉编译型代码 |
与 Go 对比:Go 的 goroutine 是 M:N 调度、无 GIL,写并发更省心;Python 受 GIL 约束,需按任务类型挑模型。
五、与本站主线的衔接
| Python 并发 | 本站对应 | 衔接文档 |
|---|---|---|
| 多进程模型 | 进程/线程生命周期 | 进程创建、CoW、IPC |
| asyncio + epoll | epoll 低延迟 | 事件驱动、多路复用 |
| GIL 与原子 | L4 内存序 | 解释器内部用锁保护状态 |
| 协程调度 | Go goroutine | 协程模型的跨语言对照 |
六、常见坑
- CPU 密集别用多线程:会被 GIL 卡成单核,改成
multiprocessing或下沉 C。 - asyncio 里别调阻塞函数:同步
time.sleep/requests会阻塞整个事件循环,要用asyncio.sleep/aiohttp。 - 多进程入口保护:
Pool等必须放在if __name__ == "__main__":下,避免子进程递归 fork。
一句话总结
GIL 让 Python 多线程只能并发不能并行——CPU 密集用多进程绕开、IO 密集用 asyncio 事件循环(epoll 驱动),按任务类型选模型,才能把机器用满。
继续:性能优化路径。