>>>PyPathPython 学习站
首页›进阶篇›kp-029
进阶篇 · Advanced lv.3 进阶 kp-029

GIL 与并发模型总览

前置知识:kp-013、kp-022

1. 一句话定义

GIL(全局解释器锁)保证 CPython 中同一时刻只有一个线程在执行 Python 字节码——因此多线程加速不了 CPU 密集任务,但 IO 等待时会释放锁;四条并发路线(线程/进程/asyncio/无 GIL)各有适用面。

2. 为什么重要

"为什么我的多线程程序没有变快"是 Python 并发的第一问。GIL 决定了选型:IO 密集用线程或 asyncio,CPU 密集用多进程。2024 起 PEP 703(free-threading)开始改变游戏规则,但主战场认知仍是本篇。

3. 前置知识

kp-001(字节码与解释器)、kp-013(阻塞 vs 惰性)。

4. 核心概念

text
                ┌─ 线程 threading ── IO 密集;GIL 下 CPU 密集无收益
任务特性 ────────┼─ 进程 multiprocessing ── CPU 密集;开销大(进程启动+序列化)
                ├─ asyncio ── 海量 IO 并发(网络);单线程协作式
                └─ free-threading(3.13 实验)── 去掉 GIL 的多线程

  • GIL 存在的原因:让 CPython 的内存管理(引用计数)线程安全且简单高效。
  • 释放时机:线程阻塞在 IO/time.sleep/C 扩展(如 NumPy 大矩阵运算)时主动让出。
  • 并发 vs 并行:并发 = 交替推进(asyncio、单核多线程);并行 = 同时推进(多进程、多核)。

5. 原理与机制

python
# 验证:CPU 密集下线程 vs 进程
import time, threading, multiprocessing

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

t0 = time.perf_counter()
ts = [threading.Thread(target=cpu_task, args=(10**7,)) for _ in range(4)]
[t.start() for t in ts]; [t.join() for t in ts]
print("threads:", time.perf_counter() - t0)      # ≈ 单线程的 4 倍耗时(GIL)

t0 = time.perf_counter()
ps = [multiprocessing.Process(target=cpu_task, args=(10**7,)) for _ in range(4)]
[p.start() for p in ps]; [p.join() for p in ps]
print("procs:", time.perf_counter() - t0)        # ≈ 单线程(真并行)

线程切换每 5ms(sys.getswitchinterval())强制轮转,即便纯计算也让四线程比单线程更慢(加上切换开销)。

6. 关键事实(模型/图示)

text
GIL 不是"锁住整个程序",是"锁住字节码执行":
  IO 等待(socket/read/sleep)→ 放锁 → 其他线程跑 → CPython 多线程仍能并发 IO
  C 扩展(NumPy/OpenCV)在 C 层运算时常自放 GIL → 真并行
free-threading(PEP 703):引用计数改为 biased reference counting,
  3.13 起作为独立构建(--disable-gil)实验发行,3.14 持续演进

7. 直观类比

GIL 像教室里唯一的麦克风:一次只有一个人(线程)能发言(执行字节码),其他人干等;但有人去走廊打电话(IO 阻塞)时会把麦克风放下。想让全员同时干活只能多开教室(多进程)——或者等学校换新音响系统(free-threading)。

8. 实例与案例

选型速查:

text
爬 1000 个 URL          → asyncio(或线程池 20-100 并发)
压缩 100 个大文件        → 多进程(CPU 密集)
调用 GPU 训练            → 单线程 + NumPy/PyTorch(C 层并行)
每秒处理万级 socket      → asyncio

9. 常见误区

  1. "GIL 使 Python 不能并发" —— 不能的是字节码级并行;IO 并发、进程并行、C 层并行都成立。
  2. 线程池跑 CPU 密集求加速 —— 无收益甚至更慢(切换开销)。
  3. 以为 GIL 免除了锁 —— 数据竞争依然存在(count += 1 非原子,check-then-act 模式会丢更新);GIL 只保单条字节码,不保业务原子性。
  4. 以为 3.13 起 Python 就无 GIL —— free-threading 是独立实验构建,主流仍是带 GIL 的 CPython。

10. 自测题

  1. 为什么多线程下载 100 个文件能显著加速,多线程算 100 个矩阵乘却不能?
  2. GIL 释放的三个典型时机?
  3. x = 0; 线程里执行 x += 1 十万次 × 10 线程,结果一定等于 100 万吗?
参考答案
  1. 下载是 IO 等待(线程放锁等网络);矩阵乘的 Python 层只是"发起调用",若算子未被 C 层并行化,每步都在抢 GIL 执行字节码。
  2. IO 阻塞、sleep/等待锁、进入释放 GIL 的 C 扩展代码。
  3. 不一定;x += 1 编译为 LOAD/ADD/STORE 多条字节码,线程切换可插在中间造成丢失更新——需要锁或原子操作。

11. 与其他知识点的关系

  • kp-030/kp-032 线程与进程:两条路线的实操篇。
  • kp-031 asyncio:第三条路线。
  • kp-039 新版本特性:free-threading 与 JIT 的最新状态。

12. 延伸阅读

  • PEP 703(Making the Global Interpreter Lock Optional)
  • Python Wiki: GlobalInterpreterLock