首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Python多线程居然比单线程还慢?是哪里出问题了

Python多线程居然比单线程还慢?是哪里出问题了

原创
作者头像
风一样的男子
发布2026-07-02 14:17:55
发布2026-07-02 14:17:55
60
举报
文章被收录于专栏:编程教程编程教程

一个让我怀疑人生的性能测试

去年有个朋友跑来找我,说他写了个多线程程序,结果慢得离谱。原代码长这样:

代码语言:javascript
复制
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}秒")

他跑出来的结果:

代码语言:javascript
复制
单线程耗时: 3.8秒
双线程耗时: 8.2秒

他盯着屏幕看了五分钟,冒出一句:"为什么两个线程反而比一个线程慢了一倍多? 是我写错了吗?"

代码没错,他遇到的是Python最著名、最坑人的那个"特性"——GIL(全局解释器锁)

今天我就把GIL这件事彻底讲清楚,包括它为什么存在、它怎么坑人、以及你该怎么绕开它。


GIL到底是什么?

GIL的全称是Global Interpreter Lock,中文叫"全局解释器锁"。它是CPython(就是最常用的那个Python实现)里的一个互斥锁

它干了一件事:保证同一时刻,只有一个线程能执行Python字节码

你可以把它想象成游乐场里最热门项目的"排队手环"——谁拿到手环,谁才能玩。玩一会儿(大约15毫秒,或者执行固定数量的字节码指令),就得把手环交出来,让下一个人玩。

这个设计让CPython的内存管理变得简单安全。Python用引用计数来管理内存,每个对象都记录自己被引用了多少次。如果没有GIL,多个线程同时修改一个对象的引用计数,会造成数据错乱,导致内存泄漏或程序崩溃。

GIL的代价:在多核CPU上,你开的多个Python线程无法真正并行执行计算任务。它们只是在同一个核上轮流跑,而且因为频繁切换,还产生了额外的开销。


为什么计算密集型任务,多线程反而更慢?

当一个计算密集型任务(比如上面那个大循环)在多线程下运行时,CPU实际上在反复做这几件事:

  1. 线程A拿到GIL,执行计算(约15毫秒)
  2. 线程A释放GIL,进入等待状态
  3. 操作系统调度线程B获得CPU
  4. 线程B获取GIL,开始执行计算
  5. 线程B释放GIL,进入等待状态
  6. 操作系统调度线程A获得CPU
  7. 线程A获取GIL,继续执行计算
  8. 循环往复……

本来一个线程可以连续计算3.8秒完成的任务,现在变成了两个线程反复争夺GIL和CPU。每次切换都有开销(保存上下文、恢复上下文、锁竞争),这些开销加起来,反而让总耗时增加了。

关键点:多线程并没有"并行"执行,只是在"并发"地抢GIL。对于纯计算任务,线程数越多,上下文切换越频繁,效率越低。


那多线程在Python里还有什么用?

有用。I/O密集型任务

比如网络爬虫、文件读写、数据库查询——这些任务大部分时间在等待外部资源(网络响应、磁盘读写)。当一个线程在等待I/O时,它会主动释放GIL,让其他线程有机会执行。

所以,对于I/O密集型任务,Python多线程确实能提速——你可以在等待一个请求返回的同时,发出另一个请求。

代码语言:javascript
复制
I/O密集型场景示例:
- 爬虫同时请求10个网页
- Web服务器同时处理多个用户请求
- 批量读写多个文件

破解GIL的三大方案

方案1:多进程(multiprocessing)

既然GIL只限制线程,那就用进程。每个进程有自己独立的GIL,可以真正并行。

代码语言:javascript
复制
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倍。

代价:进程间通信成本高,数据共享需要用队列或共享内存,代码复杂度增加。

方案2:用C扩展库

NumPy、Pandas这些库底层是C语言写的,执行计算时会释放GIL,所以多线程依然能加速。

这就是为什么数据科学领域用Python做大量矩阵运算依然很快——真正吃性能的部分是C写的。

方案3:异步编程(asyncio)

对于I/O密集型任务,asyncio可以在单线程内高并发地处理大量I/O,效率高于多线程。


好消息:无GIL的Python正在到来

Python 3.13已经提供了实验性的无GIL构建版本(自由线程,Free-Threaded)。Python 3.14进一步改进了这个特性,让它在性能和稳定性上更接近生产可用。

测试表明,对于可并行、数据独立的工作负载,无GIL版本能把执行时间缩短到原来的1/4,能耗也大幅下降。

但需要注意:

  • 单线程性能会略有下降(约5-10%)
  • 内存占用会增加(约10%,因为引入了更细粒度的锁)
  • 第三方库的兼容性还在逐步完善

结论:判断任务类型,选择合适的工具

回到开头的问题:为什么你的多线程比单线程慢?

因为你的任务是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 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一个让我怀疑人生的性能测试
  • GIL到底是什么?
  • 为什么计算密集型任务,多线程反而更慢?
  • 那多线程在Python里还有什么用?
  • 破解GIL的三大方案
    • 方案1:多进程(multiprocessing)
    • 方案2:用C扩展库
    • 方案3:异步编程(asyncio)
  • 好消息:无GIL的Python正在到来
  • 结论:判断任务类型,选择合适的工具
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档