跳到主要内容

GIL 全局解释器锁

问题

什么是 GIL?为什么 Python 有 GIL?如何绕过 GIL 的限制?

答案

GIL 是什么

GIL(Global Interpreter Lock) 是 CPython 解释器中的一把全局互斥锁。任何时刻,只有一个线程能执行 Python 字节码。

GIL 存在的原因

  1. 保护 CPython 的引用计数:CPython 使用引用计数做垃圾回收,多线程同时修改引用计数会导致数据竞争
  2. 简化 C 扩展开发:C 扩展不需要自己处理线程安全
  3. 历史原因:早期单核 CPU 时代的设计决策

GIL 的影响

CPU 密集型:多线程无法利用多核,甚至比单线程更慢(线程切换开销)

import threading
import time

def count(n):
while n > 0:
n -= 1

# 单线程
start = time.time()
count(100_000_000)
print(f"单线程: {time.time() - start:.2f}s") # ~3.5s

# 多线程(因为 GIL,不会更快)
start = time.time()
t1 = threading.Thread(target=count, args=(50_000_000,))
t2 = threading.Thread(target=count, args=(50_000_000,))
t1.start(); t2.start()
t1.join(); t2.join()
print(f"多线程: {time.time() - start:.2f}s") # ~3.8s(甚至更慢)

IO 密集型:GIL 在等待 IO 时会释放,多线程有效

import threading
import requests

def fetch(url):
return requests.get(url)

urls = ["https://example.com"] * 10

# 多线程处理 IO 密集型任务有明显加速
threads = [threading.Thread(target=fetch, args=(url,)) for url in urls]
for t in threads: t.start()
for t in threads: t.join()

GIL 释放时机

场景GIL 状态
执行 Python 字节码持有(每 5ms 检查一次是否释放)
IO 操作(网络、文件)释放
C 扩展调用(如 NumPy)可以主动释放
time.sleep()释放

绕过 GIL 的方案

方案原理适用场景
多进程每个进程有独立 GILCPU 密集型
C 扩展C 代码中可以释放 GIL计算密集
NumPy底层 C 实现,释放 GIL数值计算
asyncio单线程 IO 多路复用IO 密集型
子解释器独立 GIL(3.12+)实验性
Free-threading无 GIL(3.13+ 实验)未来方案
# 方案 1:多进程(最常用)
from multiprocessing import Pool

def cpu_task(n):
return sum(i * i for i in range(n))

with Pool(4) as pool:
results = pool.map(cpu_task, [10_000_000] * 4)

Python 3.13 Free-threading

PEP 703 引入了实验性的无 GIL 模式:

# 编译时启用
./configure --disable-gil
python -X gil=0 script.py
实验性特性

Free-threading 在 3.13 中是实验性的,许多 C 扩展还不兼容。预计 3-5 年后成为默认模式。


常见面试问题

Q1: GIL 是 Python 的特性还是 CPython 的特性?

答案

GIL 是 CPython 实现的特性,不是 Python 语言的特性。其他实现如 Jython(Java)、IronPython(.NET)没有 GIL。PyPy 目前也有 GIL,但在研究去除方案。

Q2: 有了 GIL,Python 的多线程还有用吗?

答案

有用。GIL 只影响 CPU 密集型任务。对于 IO 密集型任务(网络请求、文件操作、数据库查询),GIL 在等待 IO 时会释放,多线程能显著提升性能。

Q3: 为什么不直接去掉 GIL?

答案

  1. 向后兼容性:去掉 GIL 会导致大量 C 扩展不兼容
  2. 单线程性能:去掉 GIL 后,单线程性能可能下降(需要细粒度锁)
  3. 复杂性:引用计数的线程安全需要原子操作,增加开销

这也是 PEP 703 采用渐进策略的原因。

Q4: asyncio 和多线程的区别?

答案

对比asyncio多线程
执行方式单线程协作式多线程抢占式
切换开销极低(用户态切换)较高(内核态切换)
并发量可达数万通常几百到几千
竞态条件较少(协作式)需要锁保护
编码风格async/await传统同步代码
生态依赖需要异步库可用同步库

相关链接