首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Python的GIL锁把我坑惨了,原来多线程计算真的一点用都没有

Python的GIL锁把我坑惨了,原来多线程计算真的一点用都没有

原创
作者头像
风一样的男子
发布2026-08-24 14:56:24
发布2026-08-24 14:56:24
100
举报
文章被收录于专栏:编程教程编程教程

讲个真实的事。

去年我接到一个任务:优化一个数据处理程序。输入是100万个JSON文件,需要对每个文件做解析、特征提取,然后汇总结果。单核跑要40多分钟,太慢了。

我的电脑是8核的,心想:这不刚好用多线程吗?Python的ThreadPoolExecutor用起来多顺手,几行代码搞定。

于是我把代码改成了这样:

代码语言:javascript
复制
with ThreadPoolExecutor(max_workers=8) as executor:
    results = executor.map(process_file, files)

开开心心跑起来,看了眼CPU占用率——**只有25%**。

8个核心,只用了2个。跑完一看时间,35分钟。就快了5分钟?跟预想的8倍加速差了十万八千里。

我一度以为是代码写错了,查了半天也没发现bug。后来才知道,是GIL这个拦路虎在搞鬼。如果你也遇到过「多线程反而跑不动」的怪事,这篇就是为你写的。

什么是GIL?先把这个搞明白

GIL全称叫全局解释器锁,是CPython(就是咱们平常用的那个Python)的一个机制。

用大白话说:GIL就是一扇门,任何时候只有一个线程能通过它去执行Python代码。

不管你的CPU有多少个核,GIL保证同一时刻只有一个线程在跑Python代码。其他线程就算准备好了,也得等在门外。

这就有意思了:你开了8个线程,但同一时刻只有1个在工作。这就是为什么我的CPU占用只有25%(8核里只有1个满负荷,加上调度开销,整体大概25%)。

GIL不是Python的设计缺陷,而是一个历史选择。Python刚诞生的时候,电脑还是单核的,压根没人考虑多核并发的问题。GIL让内存管理变得简单——不用搞复杂的锁机制来保护对象,垃圾回收也好做。这个设计让CPython的实现特别轻量,也就一路走到了今天。等大家开始用多核CPU的时候,GIL已经根深蒂固了,去掉它会导致大量现有的C扩展库崩溃。

一个直观的实验:看数据说话

理论归理论,咱们动手验证一下。

先写一个计算密集型的任务——算斐波那契数列,专门消耗CPU:

代码语言:javascript
复制
import time
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

def fib(n):
    if n <= 2:
        return 1
    return fib(n-1) + fib(n-2)

def cpu_bound_task(n=35):
    return fib(n)

分别用单线程、多线程、多进程跑,看效果:

代码语言:javascript
复制
# 单线程
start = time.time()
for _ in range(8):
    cpu_bound_task(35)
print(f"单线程: {time.time() - start:.2f}s")

# 多线程(8个线程)
start = time.time()
with ThreadPoolExecutor(max_workers=8) as executor:
    list(executor.map(cpu_bound_task, [35]*8))
print(f"多线程: {time.time() - start:.2f}s")

# 多进程(8个进程)
start = time.time()
with ProcessPoolExecutor(max_workers=8) as executor:
    list(executor.map(cpu_bound_task, [35]*8))
print(f"多进程: {time.time() - start:.2f}s")

结果会让你大跌眼镜:

代码语言:javascript
复制
单线程: 4.82s
多线程: 5.01s
多进程: 1.06s

看到了吗?多线程比单线程还慢! 多出的开销来自线程切换和GIL的争抢。而多进程直接飙到了接近8倍的加速。

这组数据说明一件事:对于计算密集型任务,Python的多线程是白费力气。

换个任务,结果完全不一样

但如果任务是IO密集型的——比如网络请求、文件读写、数据库查询——结果又不同了。

再做个实验:

代码语言:javascript
复制
import time
import requests
from concurrent.futures import ThreadPoolExecutor

def io_bound_task(url):
    response = requests.get(url)
    return response.status_code

urls = ["https://www.baidu.com"] * 20

# 单线程
start = time.time()
for url in urls:
    io_bound_task(url)
print(f"单线程: {time.time() - start:.2f}s")

# 多线程
start = time.time()
with ThreadPoolExecutor(max_workers=10) as executor:
    list(executor.map(io_bound_task, urls))
print(f"多线程: {time.time() - start:.2f}s")

结果:

代码语言:javascript
复制
单线程: 4.53s
多线程: 0.61s

多线程快了7倍多!

为什么同样是多线程,一个完全没用,一个快了好几倍?

关键区别在于:IO操作会主动释放GIL。

当线程在做网络请求、读写磁盘、sleep等操作时,底层会暂时释放GIL,让其他线程有机会执行。所以在IO密集型场景下,GIL形同虚设,多线程能真正并行工作。

而计算任务不会主动释放GIL,线程一直占着锁,其他线程只能干等着。

GIL到底在锁什么?

你可能好奇:GIL到底锁的是什么东西?是CPU吗?是线程吗?

都不是。GIL锁的是Python解释器本身

当你执行a = 1 + 1这行代码时,Python解释器要做的事情:

  • 解析字节码
  • 从内存加载数字1
  • 执行加法运算
  • 把结果赋给变量a

这一系列操作,必须持有GIL才能进行。没有GIL,别的线程就不能执行任何Python代码。

这意味着什么?意味着无论你开多少线程,Python代码的执行速度不会因为核数增加而变快。计算总量是固定的,GIL只是让这些计算只能在一个核上排队执行。

GIL为什么不去掉?不是不想,是不能

你可能要问:既然GIL这么坑,Python为什么不把它去掉?

这是个好问题。实际上,Python社区讨论这个问题讨论了十几年。但GIL是CPython的根基,牵一发动全身。

去掉GIL会带来三个大麻烦:

1. 现有的C扩展库全崩。 像NumPy、Pandas、科学计算库这些,很多是用C写的,都依赖于GIL来保证内存安全。去掉GIL,这些库要重写,工作量以百万行计。

2. 单线程性能下降。 去掉GIL后,为了处理多核并发,要在每一个对象上加细粒度的锁,导致单线程程序变慢(据说会慢30%-50%)。Python的生态里有大量单线程程序,为了少数人的多核需求牺牲多数人的性能,社区不会同意。

3. 实现复杂度爆炸。 CPython的内存管理目前依赖GIL来保证引用计数的线程安全。去掉它,垃圾回收的复杂度会指数级上升,bug也会成倍增加。

所以不要指望GIL被去掉。Python官方明确表示过:GIL暂时不会消失。与其等GIL被移除,不如学会与它共存。

怎么绕过GIL?三条路可走

既然GIL绕不开,怎么写出真正利用多核的程序?

方案一:多进程

多进程是最直接的解决方案。每个进程有自己独立的Python解释器,自然也就有自己的GIL。多进程可以真正利用多核。

代码语言:javascript
复制
from concurrent.futures import ProcessPoolExecutor

with ProcessPoolExecutor(max_workers=8) as executor:
    results = executor.map(process_file, files)

但多进程也有代价:进程间数据不共享,需要序列化传递,开销比线程大。而且创建进程比创建线程慢得多。

方案二:用C扩展

如果你用的是NumPy、Pandas这些库,它们内部是C/C++写的,GIL在这些计算过程中会被释放。你只管用就行,多线程调用NumPy运算,GIL基本不碍事。

代码语言:javascript
复制
import numpy as np
import threading

# NumPy的多线程运算是有效的
# 因为计算发生在C层,GIL被释放了
def compute():
    a = np.random.rand(1000, 1000)
    b = np.random.rand(1000, 1000)
    return np.dot(a, b)

方案三:换解释器

Jython(Python的Java实现)和IronPython(Python的.NET实现)没有GIL,但它们不能运行C扩展库。PyPy也有GIL,但性能通常比CPython好。不太主流的项目,生产环境用的人极少。

所以,到底该用线程还是进程?

给你一张速查表,照着选就行:

场景

CPU密集型

IO密集型

混合型

计算为主

多进程

多线程

多进程+异步IO

频繁读写

多进程

多线程

多线程

大量数据共享

多线程(但有GIL限制)

多线程

多进程+共享内存

再精简一点:

  • 计算任务→用多进程
  • 网络/文件任务→用多线程
  • 不确定→先用多线程,如果CPU占用上不去,换成多进程

一个特别的坑:混合任务里的多线程

还有一种情况容易被忽视:函数里既有IO又有计算。

比如读取一个大文件,然后做复杂解析。读取部分是IO操作,会释放GIL;解析部分是CPU操作,又会被GIL限制住。这种情况下,多线程的收益只体现在IO部分,计算部分还是单核跑。

要解决这个问题,可以把IO和计算拆分到不同的阶段,IO部分用多线程批量读,计算部分用多进程并行处理。有点像是把流水线上的瓶颈工序单独拉出来加速。

写在最后

那次优化任务,我最后用了多进程方案。但Python的multiprocessing模块序列化开销也不小,100万个文件切分成多个批次,每个批次由一个进程处理,整体时间缩短到了8分钟。虽然没达到8倍理论加速,但也够用了。

回过头看那个「多线程完全没加速」的坑,其实是自己对GIL不够了解。多线程这东西,用对了地方是神兵利器,用错了地方是南辕北辙。

最后送你三句话,记在心里:

  • Python的多线程不能提速计算——别用它跑数学运算
  • Python的多线程适合IO等待——网络、文件、数据库用它准没错
  • 多进程才是利用多核的正道——计算密集型任务用它

知道了这些,再写并发代码的时候,心里就有底了。至少不会像我当初那样,傻傻地以为8个线程就能跑满8个核。

下次有人跟你说Python多线程没用,你可以告诉他:看场景。写写爬虫、做做Web请求,Python的多线程可香了。但要是跑计算,确实是没用。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 什么是GIL?先把这个搞明白
  • 一个直观的实验:看数据说话
  • 换个任务,结果完全不一样
  • GIL到底在锁什么?
  • GIL为什么不去掉?不是不想,是不能
  • 怎么绕过GIL?三条路可走
    • 方案一:多进程
    • 方案二:用C扩展
    • 方案三:换解释器
  • 所以,到底该用线程还是进程?
  • 一个特别的坑:混合任务里的多线程
  • 写在最后
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档