去年有个朋友跑来找我,说他写了个多线程程序,结果慢得离谱。原代码长这样:
import threading
import time
def compute():
total = 0
for i in range(100_000_000):
total += i
return total
# 单线程
start = time.time()
compute()
print(f"单线程耗时: {time.time() - start:.2f}秒")
# 两个线程并行
threads = []
start = time.time()
for _ in range(2):
t = threading.Thread(target=compute)
threads.append(t)
t.start()
for t in threads:
t.join()
print(f"双线程耗时: {time.time() - start:.2f}秒")他跑出来的结果:
单线程耗时: 3.8秒
双线程耗时: 8.2秒他盯着屏幕看了五分钟,冒出一句:"为什么两个线程反而比一个线程慢了一倍多? 是我写错了吗?"
代码没错,他遇到的是Python最著名、最坑人的那个"特性"——GIL(全局解释器锁)。
今天我就把GIL这件事彻底讲清楚,包括它为什么存在、它怎么坑人、以及你该怎么绕开它。
GIL的全称是Global Interpreter Lock,中文叫"全局解释器锁"。它是CPython(就是最常用的那个Python实现)里的一个互斥锁。
它干了一件事:保证同一时刻,只有一个线程能执行Python字节码。
你可以把它想象成游乐场里最热门项目的"排队手环"——谁拿到手环,谁才能玩。玩一会儿(大约15毫秒,或者执行固定数量的字节码指令),就得把手环交出来,让下一个人玩。
这个设计让CPython的内存管理变得简单安全。Python用引用计数来管理内存,每个对象都记录自己被引用了多少次。如果没有GIL,多个线程同时修改一个对象的引用计数,会造成数据错乱,导致内存泄漏或程序崩溃。
GIL的代价:在多核CPU上,你开的多个Python线程无法真正并行执行计算任务。它们只是在同一个核上轮流跑,而且因为频繁切换,还产生了额外的开销。
当一个计算密集型任务(比如上面那个大循环)在多线程下运行时,CPU实际上在反复做这几件事:
本来一个线程可以连续计算3.8秒完成的任务,现在变成了两个线程反复争夺GIL和CPU。每次切换都有开销(保存上下文、恢复上下文、锁竞争),这些开销加起来,反而让总耗时增加了。
关键点:多线程并没有"并行"执行,只是在"并发"地抢GIL。对于纯计算任务,线程数越多,上下文切换越频繁,效率越低。
有用。I/O密集型任务。
比如网络爬虫、文件读写、数据库查询——这些任务大部分时间在等待外部资源(网络响应、磁盘读写)。当一个线程在等待I/O时,它会主动释放GIL,让其他线程有机会执行。
所以,对于I/O密集型任务,Python多线程确实能提速——你可以在等待一个请求返回的同时,发出另一个请求。
I/O密集型场景示例:
- 爬虫同时请求10个网页
- Web服务器同时处理多个用户请求
- 批量读写多个文件既然GIL只限制线程,那就用进程。每个进程有自己独立的GIL,可以真正并行。
from multiprocessing import Pool
def compute():
total = 0
for i in range(100_000_000):
total += i
return total
with Pool(4) as pool:
results = pool.map(compute, range(4))测试结果:4个进程并行,耗时降到约1.1秒,加速比接近4倍。
代价:进程间通信成本高,数据共享需要用队列或共享内存,代码复杂度增加。
NumPy、Pandas这些库底层是C语言写的,执行计算时会释放GIL,所以多线程依然能加速。
这就是为什么数据科学领域用Python做大量矩阵运算依然很快——真正吃性能的部分是C写的。
对于I/O密集型任务,asyncio可以在单线程内高并发地处理大量I/O,效率高于多线程。
Python 3.13已经提供了实验性的无GIL构建版本(自由线程,Free-Threaded)。Python 3.14进一步改进了这个特性,让它在性能和稳定性上更接近生产可用。
测试表明,对于可并行、数据独立的工作负载,无GIL版本能把执行时间缩短到原来的1/4,能耗也大幅下降。
但需要注意:
回到开头的问题:为什么你的多线程比单线程慢?
因为你的任务是CPU密集型的,而GIL让多线程无法真正并行。
记住这张表:
任务类型 | 推荐方案 | 原因 |
|---|---|---|
CPU密集型(计算、循环) | 多进程或C扩展 | 绕开GIL |
I/O密集型(网络、文件) | 多线程或asyncio | I/O等待时释放GIL |
简单I/O并发 | asyncio | 更轻量,无GIL限制 |
你那个"双线程比单线程慢一倍"的现象,就是因为两个线程在抢GIL,切换开销大于计算收益。
解决方案:用multiprocessing,或者把计算逻辑改成numpy实现。下次别再被GIL坑了。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。