免费编程软件「python+pycharm」
链接:https://pan.quark.cn/s/48a86be2fdc0

一个想优化性能、结果越优化越慢的故事

去年有个朋友跟我吐槽,说他想用多线程加速一个数据处理任务。原代码是单线程跑的,处理100万条数据要8秒。他觉得CPU利用率太低,心想:"我有8个核,开8个线程,岂不是1秒就能跑完?"

于是他兴冲冲地改了代码,把数据平均分给8个线程并行处理。结果跑完一看,傻眼了——耗时11秒,比单线程还慢了3秒。

他发来一段代码给我看:

import threading
import time

def cpu_task(n):
    """纯计算任务:数到n"""
    total = 0
    for i in range(n):
        total += i
    return total

# 单线程跑
start = time.time()
for _ in range(4):
    cpu_task(100_000_000)
print(f"单线程: {time.time() - start:.2f}秒")

# 多线程跑
threads = []
start = time.time()
for _ in range(4):
    t = threading.Thread(target=cpu_task, args=(100_000_000,))
    threads.append(t)
    t.start()
for t in threads:
    t.join()
print(f"多线程: {time.time() - start:.2f}秒")

输出结果:

单线程: 8.42秒
多线程: 11.26秒

多线程反而更慢了

他问我:"Python的多线程是不是假的?为什么8个线程还不如1个线程?"

我说:"不是假的,但你遇到了Python最臭名昭著的那个坑——GIL(全局解释器锁)。"


GIL是个什么东西?

GIL全称是Global Interpreter Lock,是CPython(就是咱们平时用的那个Python解释器)里的一个互斥锁

它的作用是:确保同一时刻,只有一个线程能执行Python字节码

你可以把它想象成一个"令牌":哪个线程拿到了令牌,才能执行代码。执行一会儿(比如执行100条字节码指令),就把令牌放下,其他线程抢到后再接着执行。

听起来挺公平的,对吧?但这有个致命问题:在多核CPU上,多个核心明明是空闲的,但因为GIL的存在,它们只能干瞪眼,看着一个线程在跑

这就是Python多线程在计算密集型任务上表现糟糕的根本原因。


为什么GIL拖累了计算任务?

我再用一个餐厅比喻来解释:

  • 单线程:一个厨师干活。虽然慢,但没有空闲时间。

  • 多线程:8个厨师,但厨房里只有1把炒锅(GIL)。谁拿到锅,谁才能炒菜。拿到锅的厨师炒一会儿(执行一些字节码),必须放下锅让给下一个人。

结果就是:8个厨师大部分时间在抢锅等锅,真正炒菜的时间并没有增加,反而因为抢来抢去浪费了时间。

这就是多线程比单线程还慢的原因——线程切换和锁竞争的开销,抵消了多核并行带来的好处。

网上有人实测过:单线程跑5亿次循环,耗时8.4秒;两个线程各跑2.5亿次,总耗时11.2秒,反而慢了。我用上面的代码复现,结果完全一致。


GIL对哪些任务影响最大?

CPU密集型任务——也就是那些大量消耗CPU计算资源的代码,比如循环累加、数学计算、图像处理、机器学习训练等。GIL对这类任务的性能打击最严重。

那为什么Python还要保留GIL?

因为GIL的诞生,最初是为了简化内存管理。CPython用引用计数来管理内存,如果没有GIL,多个线程同时修改同一个对象的引用计数,会导致计数错误,进而引发内存泄漏或崩溃。加上GIL,就安全了,代价就是性能。


等等,那多线程在Python里还有用吗?

有用。而且非常有用。

GIL的坑,主要体现在CPU密集型任务上。对于I/O密集型任务,多线程依然能带来巨大的性能提升。

什么是I/O密集型任务? 就是那些大部分时间在等待外部资源(比如等待网络响应、等待磁盘读写)的任务。比如:

  • 网络爬虫(等待服务器返回数据)

  • 文件读写(等待磁盘)

  • 数据库查询(等待数据库返回结果)

在这些场景下,线程在等待I/O完成时会主动释放GIL,让其他线程有机会执行。所以你可以同时发起几十个网络请求,并发地等待响应,效率远高于串行一个接一个地等。


破解GIL的三大方案

如果你的任务确实是CPU密集型的,又有性能要求,有以下几条路可以走:

方案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

if __name__ == "__main__":
    with Pool(4) as pool:
        results = pool.map(cpu_task, [100_000_000] * 4)

缺点:进程间通信开销大,数据共享麻烦。

方案2:把计算密集部分下沉到C扩展

对于numpy、pandas这类底层用C/C++写的库,它们在执行计算时会释放GIL,不受这个限制。所以用numpy做矩阵运算,多线程确实是能加速的。

方案3:异步编程(asyncio)

对于I/O密集型任务,asyncio可以做到在单线程内高并发地处理大量I/O操作,效率比多线程更高,而且没有GIL的烦恼。


好消息:Python正在移除GIL

Python 3.13已经提供了实验性的无GIL构建版本(自由线程,Free-Threaded)。在Python 3.14中,这个特性进一步得到了完善。

根据一些测试,对于数据并行、互相独立的工作负载,无GIL版本可以把执行时间缩短到原来的1/4,能耗也大幅降低。有开发者实测,某个任务从5.77秒缩短到了1.36秒。

但要注意,无GIL版本目前还不是默认选项,而且有代价:

  • 单线程性能会略微下降

  • 内存占用会增加约10%(因为引入了更细粒度的锁机制)

  • 第三方库的兼容性还在逐步完善中

Python之父Guido van Rossum也提醒过:不要神话并发。很多人并行化之后反而变慢了,与其追逐热点,不如把代码的可维护性和长期演进放在更优先的位置。


记住这个坑,以后别再踩了

回到开头那个朋友的问题,我给他的建议很简单:

  1. 先判断你的任务是CPU密集型还是I/O密集型。

  2. I/O密集型:放心用threading,它会帮你提速。

  3. CPU密集型:用multiprocessing,或者换用numpy这类底层用C实现的库。如果用纯Python硬算,开再多线程也没用,甚至更慢。

GIL是CPython的一个历史性设计决策,它让Python的内存管理变得简单安全,但也付出了性能代价。理解了它,你就能在Python的并发编程里少走很多弯路。

另外,随着无GIL版本的逐步成熟,未来这个"坑"可能会越来越浅。但在那之前,记住今天的结论:Python多线程在计算密集型任务上,确实会比单线程还慢

更多推荐