
我之前在一家公司带新人,有个刚毕业的同学接了个「把日志解析脚本改成多线程加速」的需求。
第二天他兴冲冲跑过来说改完了。我让他贴 benchmark 数据上来——单线程 8 分钟,4 线程 8 分 12 秒,8 线程 8 分 30 秒。线程越多越慢。
他当场懵了:「Python 多线程不是用来加速的吗?」
我说不是。Python 多线程是用来「并发」的,不是用来「并行」的。这俩在 Python 里因为 GIL 的存在,被强行分开了。今天讲清楚 GIL 到底是个啥、它拦住了什么、什么时候用多线程有用、什么时候必须改多进程。
GIL 是个全局的「上锁」
GIL 全称 Global Interpreter Lock,全局解释器锁。CPython(你电脑里那个 python 解释器)的实现细节:任意时刻只有一个线程能执行 Python 字节码。
注意是「Python 字节码」,不是「代码」。这个区别后面会反复用到。
为啥要这把锁?历史原因——CPython 的对象内存管理(特别是引用计数)不是线程安全的。要让多线程安全访问对象,要么每个对象都加锁(性能炸裂),要么干脆全局一把大锁(实现简单)。Python 选了第二条。
结果就是:你写的多线程代码,其实并不能在多核上同时跑 Python 代码。所有线程都得抢这把锁,抢到的才能跑,跑一会儿主动释放给其他线程。
CPU 密集型:多线程不仅没用,还更慢
我那个新人遇到的就是这个情况。日志解析是纯 CPU 操作——字符串处理、正则匹配、字段提取——全程在跑 Python 字节码,全程需要 GIL。
# benchmark.py - 纯 CPU 密集任务对比
import time
import threading
def cpu_heavy(n):
total = 0
for i in range(n):
total += i * i
return total
# ===== 单线程 =====
start = time.time()
for _ in range(4):
cpu_heavy(50_000_000)
print(f'单线程: {time.time() - start:.2f}s')
# ===== 4 线程 =====
start = time.time()
threads = [threading.Thread(target=cpu_heavy, args=(50_000_000,)) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()
print(f'4 线程: {time.time() - start:.2f}s')
# 我笔记本上的结果(M1 Pro,Python 3.11):
# 单线程: 5.82s
# 4 线程: 6.31s
# 不是快了 4 倍,而是慢了一点点(线程切换开销)
为啥 4 线程反而慢?因为 4 个线程都想跑 Python 字节码,都得抢 GIL;同一时刻只有一个能跑,另外 3 个干等;GIL 还要做线程切换、上下文保存恢复——纯开销。
CPU 密集型任务,多线程在 Python 里是反优化。
IO 密集型:多线程真的能加速
但反过来。IO 操作——比如网络请求、文件读写、数据库查询——这些操作的本质是「等」。线程发起一个 requests.get(url) 后,要等几十到几百毫秒等服务器响应,这段时间没在跑 Python 字节码,GIL 会被主动释放给其他线程。
# IO 密集场景:批量下载 URL
import time
import threading
import requests
URLS = ['https://httpbin.org/delay/1'] * 10
# 单线程
start = time.time()
for url in URLS:
requests.get(url)
print(f'单线程: {time.time() - start:.2f}s') # 约 10.x s
# 10 线程
start = time.time()
threads = [threading.Thread(target=requests.get, args=(URLS[0],)) for _ in range(10)]
for t in threads: t.start()
for t in threads: t.join()
print(f'10 线程: {time.time() - start:.2f}s') # 约 1.x s
# 加速接近 10 倍,因为 9 个线程都在等网络的时候,GIL 没被占用
文件 IO、数据库、调用第三方 API——这些场景多线程效果立竿见影。所以 Python 多线程不是没用,是只在 IO 场景下有用。
CPU 密集要加速怎么办:multiprocessing
那 CPU 密集任务真的就没办法在 Python 里压榨多核了?办法是有的——开多个进程。
import time
from multiprocessing import Pool
def cpu_heavy(n):
total = 0
for i in range(n):
total += i * i
return total
if __name__ == '__main__':
start = time.time()
with Pool(4) as p:
p.map(cpu_heavy, [50_000_000] * 4)
print(f'4 进程: {time.time() - start:.2f}s')
# 我那台机器跑出来 1.6s,几乎是单线程 5.8s 的 4 倍提速
进程之间不共享 GIL(每个进程有自己的解释器、自己的 GIL),所以多进程能真正利用多核。
但代价也不小:进程启动比线程慢得多(fork 整个解释器);进程间不共享内存,传数据要序列化(pickle);内存占用是线程的好几倍。
所以选型大概是这样:
|
任务类型 |
推荐 |
|
IO 密集(网络、文件、数据库) |
threading 或 asyncio |
|
CPU 密集(计算、加密、压缩) |
multiprocessing |
|
混合型 |
asyncio + ProcessPoolExecutor |
|
真的要榨干性能的纯计算 |
numpy / Cython / 直接 C 扩展 |
最后一条值得展开。很多 CPU 密集任务其实根本不该用 Python 字节码去算。
# ❌ 慢:纯 Python 循环
result = []
for i in range(1_000_000):
result.append(i * i)
# ✅ 快 50 倍:numpy 在 C 里跑,不受 GIL 影响
import numpy as np
arr = np.arange(1_000_000)
result = arr * arr
numpy、pandas、scikit-learn 这些库的核心计算用 C/Fortran 实现,在跑 C 代码的时候会释放 GIL。所以你用 numpy 做矩阵运算,其实是间接享受到了多核——这才是 Python 在科学计算领域能赢的真正原因。
GIL 未来会消失吗
会。PEP 703 已经接受,Python 3.13 引入了实验性的 free-threaded 版本,可以在编译时关掉 GIL。3.14 / 3.15 之后可能全面铺开。
但短期内你写代码还是得当 GIL 存在。等到无 GIL 普及,目前所有线程不安全的 C 扩展都得重新适配,是个漫长的过程。
回到开头那个新人。我让他把多线程改成 multiprocessing.Pool,4 进程,跑 2 分钟出结果——单线程 8 分钟,4 进程 2 分钟,提速 4 倍。他这才理解 GIL 到底是个什么东西。
简单总结:等 IO 用线程,吃 CPU 用进程,搞算法上 numpy。 这三句话能解决 95% 的 Python 性能问题。
以上就是“Python 的 GIL 到底拦住了什么,多线程为啥没提速”的详细内容,想要了解更多Python教程欢迎持续关注编程学习网。
扫码二维码 获取免费视频学习资料

- 本文固定链接: http://www.phpxs.com/post/14396/
- 转载请注明:转载必须在正文中标注并保留原文链接
- 扫码: 扫上方二维码获取免费视频资料