首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >从入门到放弃,Python的GIL锁比我想象的还要‘坑’

从入门到放弃,Python的GIL锁比我想象的还要‘坑’

原创
作者头像
风一样的男子
发布2026-09-01 13:55:30
发布2026-09-01 13:55:30
50
举报
文章被收录于专栏:编程教程编程教程

那个让我怀疑人生的性能优化

两年前,我接了一个任务:优化一个数据处理程序。

现有代码是单线程跑的,处理200万条数据要花12秒。我看了看服务器配置——8核。心想这不简单吗?开8个线程并行处理,理论上1.5秒就能跑完。

我信心满满地改了代码:

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

多线程反而更慢了?我盯着屏幕看了五分钟,不敢相信自己的眼睛。

后来又做了一个测试:在4核CPU上计算100万以内的质数,单线程3.2秒,4线程反而要6.1秒。线程越多越慢,这个结果让我彻底懵了。

后来我才知道,这不是Python的bug,这是Python最著名的“特性”——GIL(全局解释器锁)。


GIL到底是什么鬼

GIL的全称是Global Interpreter Lock,中文叫“全局解释器锁”。

它是CPython解释器(就是我们平时用的那个Python)里的一个互斥锁机制。它的作用简单粗暴:同一时刻,只有一个线程能执行Python字节码

你可以把它想象成一把“令牌”——哪个线程拿到令牌,才能执行代码。执行一会儿就把令牌放下,让其他线程去抢。

也就是说,即使你的电脑有8核16核,跑Python多线程程序的时候,同一时间只有一个核心在工作,其他核心都在旁边看着。

听到这儿你可能想骂人了:“那Python的多线程不就是个摆设吗?”

别急,先搞清楚它为什么存在。


GIL不是设计失误,是历史选择

很多人以为GIL是Python的一个愚蠢设计失误,其实不是。

时间倒回到上世纪90年代,Python刚诞生的时候,计算机基本都是单核的,多核CPU是后来的事。当时Python的设计者Guido van Rossum面临一个很现实的问题:如何实现内存管理?

Python的内存管理用的是引用计数——每个对象都有一个计数器,记录有多少个地方引用了它。当计数器归零时,对象就被回收了。

问题来了:在多线程环境下,如果两个线程同时修改同一个对象的引用计数,就会出现竞争条件——计数可能只增加了一次而不是两次,导致对象永远无法被回收,造成内存泄漏。

举个例子:假设有3个线程同时持有对同一个Python对象的引用,此时该对象的引用计数为3。当一个线程释放对该对象的引用时,计数值降为2。但如果两个线程同时释放引用,引用计数可能只减少一次而不是两次,导致最终计数为2而不是1——这个对象永远无法被回收。

解决方案有两个:

  • 方案一:给每个对象单独加锁。性能好但实现极其复杂,容易死锁。
  • 方案二:在整个解释器层面加一把大锁。任何线程执行Python代码都必须先拿到这把锁。实现简单,性能在单核时代也完全够用。

Python选择了方案二,这就是GIL的由来。

在单核年代,这个设计非常合理。多线程其实是通过时间片轮换来模拟“同时运行”的,GIL并没有造成实质性的性能损失。直到多核CPU普及,这个设计才变成了一个问题。

打个比方:GIL就像一条单车道隧道的交通信号灯。车不多的时候,有信号灯反而更安全。但车流量大了之后,明明双向八车道的高速公路,到了隧道口还是只能一辆一辆地过。


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

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

写一个计算密集型的任务——算斐波那契数列,专门消耗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")

结果呢?

  • 单线程:约6.1秒
  • 多线程(8线程):约6.7秒,比单线程还慢。多出来的开销来自线程切换和GIL的争抢
  • 多进程(8进程):约0.9秒,接近8倍加速

多进程直接飙到了接近8倍的加速,而多线程比单线程还慢

原因很简单:四个线程轮流抢GIL,频繁的线程切换带来了额外开销。每个线程拿到GIL后执行一小会儿就被迫让出,这种“上下文切换”是有成本的。


多线程到底有没有用?

有用,但要看场景

GIL只限制Python字节码的执行。当线程执行I/O操作(比如网络请求、文件读写、数据库查询)时,它会主动释放GIL。其他等待的线程就可以趁机获取GIL并执行。

这就好比一个食堂只有一个打饭窗口(GIL)。如果你要做的只是“站在窗口前等饭”(I/O等待),那多几个人排队也没问题——反正大家都在等,谁先谁后差别不大。但如果你要做的是一人霸占窗口做复杂操作(CPU计算),那其他人就只能干等着。

I/O密集型任务:多线程非常有效。

比如写一个爬虫,同时请求100个网页。每个请求发出后,线程就在等网络响应,这时候GIL是释放的,其他线程可以趁机执行。100个请求并发,总耗时约等于最慢的一个请求的时间,而不是100个请求的时间之和。

CPU密集型任务:多线程基本没用,甚至更慢。

比如大规模数学计算、图像处理、数据解析。每个线程都在拼命算,都在抢GIL,频繁切换带来的开销反而让总时间变长了。

一个简单的判断标准:你的程序大部分时间在等什么?

  • 等网络、等磁盘、等数据库 → 用多线程
  • 等CPU计算 → 别用多线程,用多进程

关于GIL的三个常见误解

误解一:Python多线程完全没用

只对了一半。

对于纯CPU密集型计算,GIL确实让多线程没法并行。但I/O密集型任务是另一回事。一个线程阻塞不影响其他线程,并发效果相当好。

我后来用多线程写过爬虫,8个线程并发请求,速度比单线程快了7倍多。多线程不是没用,是你没用对地方

误解二:GIL是Python语言的设计缺陷

这是CPython解释器的实现细节,不是Python语言规范的一部分。Jython(基于JVM)和IronPython(基于.NET)都没有GIL。

当年设计CPython时单核CPU是主流,在简单性、安全性和性能之间选GIL是合理的工程权衡

误解三:有了GIL,就不用操心线程安全了

这个最危险

GIL只保证了解释器级别的字节码执行和基础引用计数的原子性,你自己的业务逻辑它管不了

一个典型例子:i += 1

看着像一步操作,实际上对应“读取i的值 → 计算i+1 → 写回i”三个字节码步骤,多线程下照样数据竞争:

代码语言:javascript
复制
import threading

class UnsafeCounter:
    def __init__(self):
        self.value = 0
    def increment(self):
        # 这行代码在字节码层面不是原子的
        self.value = self.value + 1

counter = UnsafeCounter()
def worker():
    for _ in range(100000):
        counter.increment()

threads = []
for _ in range(5):
    t = threading.Thread(target=worker)
    threads.append(t)
    t.start()
for t in threads:
    t.join()

print(f"预期结果: 500000, 实际结果: {counter.value}")
# 多次运行,实际结果几乎每次都小于500000

GIL不是万能锁,该加锁的地方还是要加锁。


那几个让我加班的坑

坑一:在Windows上瞎用多进程

多进程在Windows上有个大问题——进程启动方式跟Linux不一样。Windows用的是spawn,每次创建进程都要重新导入模块,开销巨大。

如果你在Windows上跑多进程,创建进程本身可能比计算还慢。生产环境尽量用Linux跑多进程

坑二:进程间传数据要序列化

多进程之间不共享内存,数据传递需要序列化(pickle)。如果你要传的数据很大,序列化本身就要花不少时间。

所以多进程适合“任务重、数据轻”的场景。每个任务计算量大,但输入输出数据小。

坑三:不是所有第三方库都释放GIL

有些C扩展库在执行时不释放GIL,即使在做I/O操作也不放。这意味着即使你的任务是I/O密集型的,如果用了某些不释放GIL的库,多线程照样跑不起来。

用之前查一下文档,或者做个简单测试。

坑四:进程数不是越多越好

进程数超过CPU核心数之后,加速效果就不明显了,反而会增加调度开销。一般进程数等于CPU核心数就够了。


绕过GIL的几条路

方案一:多进程(multiprocessing)

每个进程有自己的Python解释器和内存空间,互不干扰。每个进程都有自己的GIL,可以跑在不同的CPU核心上,真正实现并行。

适用场景:CPU密集型任务。

代价:进程创建开销大,进程间通信需要序列化。

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

with ProcessPoolExecutor(max_workers=8) as executor:
    results = executor.map(cpu_task, data)

方案二:用C扩展(NumPy等)

像NumPy这样的库,核心计算是用C写的,不经过Python字节码,不受GIL限制。

如果你的计算任务能用NumPy、Pandas这些库实现,直接用就好,不用操心GIL。

方案三:异步编程(asyncio)

单线程+事件循环,适合高并发I/O场景。比多线程更轻量,能支撑的并发量更大。

适用场景:大量I/O操作,比如Web服务器、爬虫。

方案四:换解释器

Jython(基于JVM)、IronPython(基于.NET)都没有GIL。PyPy也支持分代GIL以提高并发性能。

不过这些解释器的生态和兼容性跟CPython有差异,生产环境用之前要评估清楚。


Python 3.13:GIL终于可以关了

说了这么多GIL的“坏话”,好消息是——Python 3.13开始,GIL终于可以关了

从Python 3.13发布版开始,CPython支持一种叫 “自由线程”(free threading) 的构建模式,禁用了全局解释器锁(GIL),允许多个线程在可用的CPU核心上并行运行。

不过目前还是实验性的。你需要用--disable-gil选项从源码编译Python,或者使用专门的python3.13t可执行文件。

自由线程的Python在执行时,可以选择性地启用或禁用GIL,通过环境变量PYTHON_GIL或命令行选项-X gil来控制。

早期基准测试显示,在M4 MacBook Air上,自由线程版本相比带GIL的版本,能带来约2.83倍的性能提升。在某些场景下,甚至能达到8-10倍的加速

不过要注意:自由线程模式目前有个已知的单线程性能折损。因为去掉了GIL之后,解释器内部需要加更多细粒度的锁来保证线程安全,这些锁本身也有开销。在单线程场景下,可能会比带GIL的版本慢一些。

这个特性至少要到未来某个版本才会默认禁用GIL。目前建议在非生产环境下实验和测试。


说穿了就一句话

GIL是CPython的一个历史遗留设计,在单核时代是合理的工程选择,到了多核时代成了性能瓶颈。

但它不是洪水猛兽。理解它、利用它、绕过它,而不是抱怨它。

  • I/O密集型 → 用多线程,GIL不是问题
  • CPU密集型 → 用多进程或C扩展,绕过GIL
  • 高并发I/O → 用asyncio,比多线程更轻量
  • 追求极致并行 → 关注Python 3.13+的自由线程版本

那个让我怀疑人生的周五下午之后,我做了一张小卡片贴在显示器边上:

问:我的程序在等什么?

  • 等网络/磁盘 → 多线程
  • 等CPU计算 → 多进程

希望你能避开我踩过的这些坑。GIL不是Python的“缺陷”,它是Python历史的一部分。理解它,你才能真正用好Python的并发。

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

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

目录
  • 那个让我怀疑人生的性能优化
  • GIL到底是什么鬼
  • GIL不是设计失误,是历史选择
  • 一个直观的实验:看数据说话
  • 多线程到底有没有用?
  • 关于GIL的三个常见误解
    • 误解一:Python多线程完全没用
    • 误解二:GIL是Python语言的设计缺陷
    • 误解三:有了GIL,就不用操心线程安全了
  • 那几个让我加班的坑
    • 坑一:在Windows上瞎用多进程
    • 坑二:进程间传数据要序列化
    • 坑三:不是所有第三方库都释放GIL
    • 坑四:进程数不是越多越好
  • 绕过GIL的几条路
    • 方案一:多进程(multiprocessing)
    • 方案二:用C扩展(NumPy等)
    • 方案三:异步编程(asyncio)
    • 方案四:换解释器
  • Python 3.13:GIL终于可以关了
  • 说穿了就一句话
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档