Python 的 GIL(全局解释器锁)是一把进程级锁,保证同一时刻只有一个线程在执行 Python 字节码。所以 CPU 密集型的多线程 Python 程序跑不满多核,只在单核上交替;而 I/O 密集型(网络、磁盘)的多线程仍能靠等待时间让出这把全局锁获得并发收益。动手写多线程前,先分清你的任务是 CPU 密集还是 I/O 密集,比急着加线程重要得多——选错通道,多线程不仅没加速,反而可能因为切换开销更慢。

先看结论
- GIL 锁的是字节码执行,不是你的代码:同一进程内,任意时刻只有一个线程在跑 Python 字节码。
- CPU 密集型多线程几乎没收益:计算任务再多核也挤在单核交替,甚至可能更慢(线程切换开销)。
- I/O 密集型多线程有收益:线程在等网络/磁盘时会释放 GIL,别的线程得以推进。
- 要真并行算 CPU 任务,用多进程:multiprocessing 每个进程一把 GIL,才能吃满多核。
- Python 3.13 起有实验性无 GIL 构建,但默认仍带 GIL,想关要单独装 free-threaded 版本。
一、先弄清 GIL 到底锁了什么
前置条件:你用 CPython(python 命令默认就是)写过多线程代码,并疑惑过「为什么开了 8 个线程还是只用一个核」。本文结论针对 CPython;Jython、IronPython 没有 GIL,行为不同。
GIL 是 CPython 解释器的一把全局锁,作用范围是「一个进程」。它保证同一个进程里,任何时刻只有一个线程能执行 Python 字节码。注意它锁的是「字节码执行权」,不是某段具体代码——你写的每一行 Python,最终都变成字节码,都得抢这把锁。
这带来一个反直觉的事实:你开 8 个线程做密集计算,在 CPython 里它们并不是 8 核并行,而是轮流抢锁、在单核上交替跑。所以「线程数 = 并行度」这个直觉在 Python 里不成立。如果你还想补一下 Python 的运行模型,可以先过一遍 Python3 教程,把解释器、字节码、线程调度这几块认清楚,再回来对照下面的取舍。
实际写并发代码前,先确认本地装的是 CPython 还是别的实现:绝大部分教程和服务器默认都是 CPython,它的 GIL 行为才有代表性;Jython、IronPython 没有 GIL,但生产里很少见。再确认你的任务是 CPU 密集还是 I/O 密集——纯计算、编码、图像处理属于前者,网络请求、磁盘读写、数据库查询属于后者,这两类对多线程的反应完全不同。把任务定性错了,后面加再多线程都是白费。如果你手头有一段现成循环,先单独跑一遍看它吃满几个核,比直接套多线程模板更靠谱。
二、CPU 密集与 I/O 密集的区别一次看清
2.1 场景一:CPU 密集型,多线程基本白搭
来看一个纯计算的例子,两个线程各自做大量加法:
import threading
def cpu_task():
total = 0
for _ in range(10_000_000):
total += 1
t1 = threading.Thread(target=cpu_task)
t2 = threading.Thread(target=cpu_task)
t1.start(); t2.start()
t1.join(); t2.join()
这段在 CPython 下,两线程抢同一把全局锁,实际是串行交替,耗时和不加线程几乎一样,甚至更慢。适用边界:只要任务是纯计算,就别用 threading,直接上 multiprocessing。这是新手最容易踩的坑。
2.2 场景二:I/O 密集型,多线程确实有收益
换成等网络的场景就完全不同:
import threading, requests
def io_task():
requests.get("https://example.com") # 等待期间释放 GIL
ts = [threading.Thread(target=io_task) for _ in range(10)]
for t in ts: t.start()
for t in ts: t.join()
线程在等响应时会释放 GIL,其他线程趁机推进,总耗时接近「单次最慢请求」而非「十次之和」。这就是多线程在爬虫、API 聚合里好用的原因。
2.3 场景三(反例):想真并行,多进程或换语言
CPU 任务要吃满多核,正确姿势是多进程:
from multiprocessing import Process
p1 = Process(target=cpu_task)
p2 = Process(target=cpu_task)
p1.start(); p2.start()
p1.join(); p2.join()
每个进程有独立 GIL,才能真正并行。失败处理:多进程间不共享内存,要传数据得用队列或管道(Queue、Pipe),直接共享全局变量会各改各的。另一个方向是 Python 3.13 的实验性 free-threaded 构建(编译时关掉 GIL),但生态兼容性还在成熟中,生产上别轻易上。涉及更细的语法与性能剖析时,Python 速查手册 能帮你快速回查内建函数与复杂度。

三、最小判断表与验证
把上面的取舍收成一张表:
| 任务类型 | 推荐方案 | 原因 |
|---|---|---|
| CPU 密集(计算、编码) | 多进程 / 换 Rust、C 扩展 | 多线程抢一把全局锁,无法并行 |
| I/O 密集(网络、磁盘) | 多线程 / asyncio | 等待时释放 GIL,并发有效 |
| 既要算又要并发 | 多进程 + 线程混合 | 进程吃满核,线程管 I/O |
验证与失败处理:写完后用 time 跑一遍单线程和多线程版本对比耗时;若 CPU 密集任务多线程没变快甚至变慢,基本就是撞上 GIL,改用 multiprocessing。边界要记两条:第一,GIL 是 CPython 特有,Jython、IronPython 没有;第二,numpy 这类底层用 C 实现的库,在计算时释放 GIL,所以矩阵运算能并行,别和纯 Python 循环混为一谈。

总结
Python 的 GIL 是一把进程级全局锁,保证同一时刻只有一个线程执行 Python 字节码。它让 CPU 密集型多线程无法真并行(只在单核交替),却不妨碍 I/O 密集型多线程靠等待释放锁获得并发收益。CPU 任务想并行请用多进程,I/O 任务用多线程或 asyncio 即可;Python 3.13 的实验性无 GIL 构建虽已出现,但生产落地仍需谨慎。动笔写多线程前,先给你的任务定性,比加线程本身更重要。
常见问题
Q:为什么我的多线程 Python 没变快?
多半是 CPU 密集型任务。CPython 的 GIL 让同一进程内的线程轮流抢锁、单核交替执行,计算量没有并行,反而多了切换开销。换成 multiprocessing 或多进程方案才能吃满多核。
Q:爬虫开了很多线程确实快了,矛盾吗?
不矛盾。爬虫是典型 I/O 密集型,线程在等网络响应时释放 GIL,其他线程趁机工作,所以总耗时大幅缩短。区别就在「等不等外部」。
Q:asyncio 和多线程哪个更适合 I/O?
都是 I/O 并发的合法方案。多线程模型直观、能跑阻塞调用;asyncio 单线程事件循环、需要异步库配合,但上下文切换更轻。简单并发用线程,高并发长连接考虑 asyncio。
Q:Python 3.13 关掉 GIL 后能直接受益吗?
默认构建仍带 GIL。只有专门安装的 free-threaded 版本才无 GIL,且部分 C 扩展尚未适配。生产环境建议先充分验证生态兼容性,不要贸然切换。
延伸学习
- 想把 Python 这块系统补齐,可以先跟一遍 Python3 爬虫入门 里并发抓取的部分,理解 I/O 并发怎么落地。
- 协议与底层安全相关的概念怎么理解,这篇笔记 的拆解思路可以借用。
- 写累了想就地格式化代码,Python 代码格式化 能直接帮你统一风格。

TRAE-AI编程



