免费编程软件「python+pycharm」
链接:https://pan.quark.cn/s/48a86be2fdc0
一个让我怀疑人生的性能测试
两年前,我接了一个任务:优化一个数据处理程序。现有代码是单线程跑的,处理200万条数据要花12秒。我想着服务器是8核的,开8个线程并行处理,理论上1.5秒就能跑完。
我信心满满地改了代码:
import threading
import time
def cpu_task(n):
"""纯计算任务:累加"""
total = 0
for i in range(n):
total += i
return total
# 单线程
start = time.time()
for _ in range(4):
cpu_task(50_000_000)
print(f"单线程: {time.time() - start:.2f}秒")
# 4个线程并行
threads = []
start = time.time()
for _ in range(4):
t = threading.Thread(target=cpu_task, args=(50_000_000,))
threads.append(t)
t.start()
for t in threads:
t.join()
print(f"4线程: {time.time() - start:.2f}秒")
跑完一看结果:
单线程: 10.21秒
4线程: 12.58秒
多线程反而更慢了? 我盯着屏幕看了五分钟,不敢相信自己的眼睛。
后来我才知道,这不是Python的bug,这是Python最著名的"特性"——GIL(全局解释器锁)。
GIL是什么?为什么会有这个东西?
GIL的全称是Global Interpreter Lock,是CPython(也就是我们平时用的那个Python解释器)里的一个互斥锁。
它的作用很直接:同一时刻,只有一个线程能执行Python字节码。
你可以把它想象成一把"令牌"——哪个线程拿到令牌,才能执行代码。执行一会儿(比如运行15毫秒,或者执行一定数量的字节码指令),就把令牌放下,让其他线程去抢。
为什么要有GIL?
原因很简单:Python的内存管理用的是引用计数——每个对象都记录着被引用的次数。如果两个线程同时修改同一个对象的引用计数,没有加锁保护,计数就可能出错,导致程序崩溃。
为了简化内存管理的复杂度,Python设计者决定用一把全局锁来保护一切。这个决定让Python在单核时代运行得很好,但到了多核时代,问题就暴露了。
多核CPU上,GIL是怎么"坑"你的?
在单核CPU上,GIL其实没什么问题——反正同一时间只有一个核心在工作。但到了多核时代,问题就来了。
想象一下:你有8个CPU核心,开了8个Python线程做计算。GIL只允许一个线程执行Python字节码,其他7个核心只能干瞪眼等着。
更糟糕的是线程切换的开销:
当线程A执行了一段时间,主动释放GIL,准备让线程B上场。但注意,在线程B被操作系统唤醒并拿到GIL之前,线程A可能已经把GIL又抢回去了——因为线程A还在就绪状态,离CPU更近。
这就导致了一个尴尬的局面:多个核心忙来忙去,大部分时间都在"抢锁"和"等锁",真正干活的时间没增加多少。
实验结果也证明了这一点。有研究者用RSA加密算法做测试,在6核机器上跑:1个线程耗时1.98秒,6个线程耗时居然也是2.1秒左右,几乎没有任何提升。
什么样的任务不会被GIL影响?
I/O密集型任务。
如果你的程序大部分时间在等待网络响应、读写文件、查询数据库,这些操作会主动释放GIL,其他线程就能趁机执行。
所以,对于Web服务器、爬虫、文件处理这类任务,Python多线程是有效果的。
CPU密集型任务——比如循环累加、数学计算、数据处理——GIL就成了瓶颈。这类任务才是"多线程跑不满CPU"的元凶。
有一个例外:如果底层用的是C扩展库(比如NumPy),这些库在执行计算时会主动释放GIL,所以多线程依然能加速。
怎么办?绕过GIL的三种方案
方案1:多进程(multiprocessing)
既然GIL只锁线程,那就用进程。
每个进程有自己独立的Python解释器和GIL,多个进程可以真正并行地跑在不同的CPU核心上。
from multiprocessing import Pool
def cpu_task(n):
total = 0
for i in range(n):
total += i
return total
with Pool(4) as pool:
results = pool.map(cpu_task, [50_000_000] * 4)
实际测试中,多进程在4核机器上能达到接近3.7倍的加速比。
代价:每个进程有独立的内存空间,数据共享需要序列化和进程间通信(IPC),内存占用会显著增加。
方案2:用C扩展库
如果你用的是NumPy这类底层用C/C++写的库,它们执行时会释放GIL,可以充分利用多核。
这也是为什么数据科学领域用Python做计算依然很快——因为真正吃计算的部分是C写的。
方案3:asyncio异步编程
对于I/O密集型任务,asyncio可以在单线程内实现高并发,效率比多线程更高,而且没有GIL的烦恼。
好消息:Python正在移除GIL
Python 3.13已经提供了实验性的无GIL版本(free-threaded build)。在Python 3.14中,这个特性进一步完善。
测试表明,对于可并行且数据独立的工作负载,无GIL版本能把执行时间缩短到原来的1/4,能耗也显著降低。
但要注意代价:
- 单线程性能会下降约5-10%
- 内存占用会增加约10%(引入了更细粒度的锁机制)
- 第三方库的兼容性还在逐步完善中
记住这个结论
- I/O密集型任务 → 放心用
threading,多线程能提速 - CPU密集型任务 → 用
multiprocessing或C扩展,纯Python多线程不仅不加速,可能还更慢 - 原因就是GIL——CPython解释器的全局锁,让多线程无法真正并行
那个让我怀疑人生的性能测试,后来用multiprocessing改写了。4个进程并行,耗时从10秒降到了2.8秒。核心原因就是绕开了GIL。
理解了GIL,你就能在Python的并发编程里少走很多弯路。下次有人问你"为什么Python多线程跑不满CPU",你就知道答案了——不是线程不够多,是GIL在中间当"交警",只允许一辆车通过。