编程学习网 > 编程语言 > Python > Python 的 GIL 到底拦住了什么,多线程为啥没提速
2026
08-07

Python 的 GIL 到底拦住了什么,多线程为啥没提速


我之前在一家公司带新人,有个刚毕业的同学接了个「把日志解析脚本改成多线程加速」的需求。

第二天他兴冲冲跑过来说改完了。我让他贴 benchmark 数据上来——单线程 8 分钟,4 线程 8 分 12 秒,8 线程 8 分 30 秒。线程越多越慢。 

他当场懵了:「Python 多线程不是用来加速的吗?」 

我说不是。Python 多线程是用来「并发」的,不是用来「并行」的。这俩在 Python 里因为 GIL 的存在,被强行分开了。今天讲清楚 GIL 到底是个啥、它拦住了什么、什么时候用多线程有用、什么时候必须改多进程。

GIL 是个全局的「上锁」

GIL 全称 Global Interpreter Lock,全局解释器锁。CPython(你电脑里那个 python 解释器)的实现细节:任意时刻只有一个线程能执行 Python 字节码。 

注意是「Python 字节码」,不是「代码」。这个区别后面会反复用到。 

为啥要这把锁?历史原因——CPython 的对象内存管理(特别是引用计数)不是线程安全的。要让多线程安全访问对象,要么每个对象都加锁(性能炸裂),要么干脆全局一把大锁(实现简单)。Python 选了第二条。 

结果就是:你写的多线程代码,其实并不能在多核上同时跑 Python 代码。所有线程都得抢这把锁,抢到的才能跑,跑一会儿主动释放给其他线程。

CPU 密集型:多线程不仅没用,还更慢 

我那个新人遇到的就是这个情况。日志解析是纯 CPU 操作——字符串处理、正则匹配、字段提取——全程在跑 Python 字节码,全程需要 GIL。 

# benchmark.py - 纯 CPU 密集任务对比

import time

import threading

 

def cpu_heavy(n):

    total = 0

    for i in range(n):

        total += i * i

    return total

 

# ===== 单线程 =====

start = time.time()

for _ in range(4):

    cpu_heavy(50_000_000)

print(f'单线程: {time.time() - start:.2f}s')

 

# ===== 4 线程 =====

start = time.time()

threads = [threading.Thread(target=cpu_heavy, args=(50_000_000,)) for _ in range(4)]

for t in threads: t.start()

for t in threads: t.join()

print(f'4 线程: {time.time() - start:.2f}s') 

# 我笔记本上的结果(M1 Pro,Python 3.11):

# 单线程: 5.82s

# 4 线程: 6.31s

# 不是快了 4 倍,而是慢了一点点(线程切换开销) 

为啥 4 线程反而慢?因为 4 个线程都想跑 Python 字节码,都得抢 GIL;同一时刻只有一个能跑,另外 3 个干等;GIL 还要做线程切换、上下文保存恢复——纯开销。 

CPU 密集型任务,多线程在 Python 里是反优化。 

IO 密集型:多线程真的能加速 

但反过来。IO 操作——比如网络请求、文件读写、数据库查询——这些操作的本质是「等」。线程发起一个 requests.get(url) 后,要等几十到几百毫秒等服务器响应,这段时间没在跑 Python 字节码,GIL 会被主动释放给其他线程。

# IO 密集场景:批量下载 URL

import time

import threading

import requests 

URLS = ['https://httpbin.org/delay/1'] * 10 

# 单线程

start = time.time()

for url in URLS:

    requests.get(url)

print(f'单线程: {time.time() - start:.2f}s')   # 约 10.x s 

# 10 线程

start = time.time()

threads = [threading.Thread(target=requests.get, args=(URLS[0],)) for _ in range(10)]

for t in threads: t.start()

for t in threads: t.join()

print(f'10 线程: {time.time() - start:.2f}s')  # 约 1.x s 

# 加速接近 10 倍,因为 9 个线程都在等网络的时候,GIL 没被占用 

文件 IO、数据库、调用第三方 API——这些场景多线程效果立竿见影。所以 Python 多线程不是没用,是只在 IO 场景下有用。 

CPU 密集要加速怎么办:multiprocessing 

那 CPU 密集任务真的就没办法在 Python 里压榨多核了?办法是有的——开多个进程。 

import time

from multiprocessing import Pool 

def cpu_heavy(n):

    total = 0

    for i in range(n):

        total += i * i

    return total

 

if __name__ == '__main__':

    start = time.time()

    with Pool(4) as p:

        p.map(cpu_heavy, [50_000_000] * 4)

    print(f'4 进程: {time.time() - start:.2f}s')

 

# 我那台机器跑出来 1.6s,几乎是单线程 5.8s 的 4 倍提速

进程之间不共享 GIL(每个进程有自己的解释器、自己的 GIL),所以多进程能真正利用多核。 

但代价也不小:进程启动比线程慢得多(fork 整个解释器);进程间不共享内存,传数据要序列化(pickle);内存占用是线程的好几倍。 

所以选型大概是这样:

任务类型

推荐

IO 密集(网络、文件、数据库)

threading 或 asyncio

CPU 密集(计算、加密、压缩)

multiprocessing

混合型

asyncio + ProcessPoolExecutor

真的要榨干性能的纯计算

numpy / Cython / 直接 C 扩展

最后一条值得展开。很多 CPU 密集任务其实根本不该用 Python 字节码去算。

# ❌ 慢:纯 Python 循环

result = []

for i in range(1_000_000):

    result.append(i * i) 

# ✅ 快 50 倍:numpy 在 C 里跑,不受 GIL 影响

import numpy as np

arr = np.arange(1_000_000)

result = arr * arr

numpy、pandas、scikit-learn 这些库的核心计算用 C/Fortran 实现,在跑 C 代码的时候会释放 GIL。所以你用 numpy 做矩阵运算,其实是间接享受到了多核——这才是 Python 在科学计算领域能赢的真正原因。

GIL 未来会消失吗

会。PEP 703 已经接受,Python 3.13 引入了实验性的 free-threaded 版本,可以在编译时关掉 GIL。3.14 / 3.15 之后可能全面铺开。 

但短期内你写代码还是得当 GIL 存在。等到无 GIL 普及,目前所有线程不安全的 C 扩展都得重新适配,是个漫长的过程。 

回到开头那个新人。我让他把多线程改成 multiprocessing.Pool,4 进程,跑 2 分钟出结果——单线程 8 分钟,4 进程 2 分钟,提速 4 倍。他这才理解 GIL 到底是个什么东西。

简单总结:等 IO 用线程,吃 CPU 用进程,搞算法上 numpy。 这三句话能解决 95% 的 Python 性能问题。

以上就是“Python 的 GIL 到底拦住了什么,多线程为啥没提速的详细内容,想要了解更多Python教程欢迎持续关注编程学习网。 

扫码二维码 获取免费视频学习资料

Python编程学习

查 看2022高级编程视频教程免费获取