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

第二天他兴冲冲跑过来说改完了。我让他贴 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 到底拦住了什么,多线程为啥没提速”的详细内容