Python GIL 完全解析

Global Interpreter Lock — 为什么你的多线程代码跑不满 CPU?

问题 1
GIL 到底是什么?
GIL(Global Interpreter Lock,全局解释器锁)是 CPython 解释器中的一个互斥锁(Mutex),它保证同一时刻只有一个线程可以执行 Python 字节码

换句话说:即使你启动了 10 个线程,在 CPython 里,它们也没法真正"同时"跑在多个 CPU 核心上。

🔒 GIL 工作机制可视化

线程 A 请求执行
GIL 空闲,线程 A 获取锁,开始执行 Python 字节码
线程 A 运行中(持有 GIL)
其他所有线程(B、C、D...)阻塞等待,无法执行字节码
遇到 I/O 或超时(默认 5ms)
线程 A 主动释放 GIL,其他线程开始竞争
线程 B 获取 GIL
线程 B 开始执行,线程 A 回到等待队列
CPU 密集型场景
线程不断竞争 GIL,带来锁开销,反而比单线程更慢(GIL 抖动)
问题 2
为什么 GIL 会存在?不能去掉吗?
GIL 的存在是 CPython 历史设计的一个权衡(Trade-off),核心原因是:CPython 的内存管理不是线程安全的

🔧 根本原因:引用计数(Reference Counting)非线程安全

对象被引用
引用计数 +1
引用消失
引用计数 -1
计数 = 0 ?
释放内存
问题场景:如果有两个线程同时对一个对象做"引用计数 -1",可能因为竞态条件(Race Condition)导致计数错误,对象没有被正确释放(内存泄漏),或者被重复释放(崩溃)。

解决方案有两种:
给每个对象加锁(细粒度锁)→ 锁开销巨大,且容易死锁
给整个解释器加一把大锁(GIL)→ 简单、稳定、单线程性能高

Guido van Rossum 的选择:方案 ②。因为 Python 诞生时(1991年)多核 CPU 还不普及,GIL 是合理取舍。

⚡ 为什么至今没去掉 GIL?

兼容性地狱:无数 C 扩展库(NumPy、Pandas、PyTorch...)都假设 GIL 存在,去掉 GIL 意味着它们全部要重写。
单线程性能下降:细粒度锁会让单线程代码变慢(有些测试显示慢 30%+)。
Python 3.13 开始实验性支持 no-GIL:需要在编译时开启 --disable-gil,还不能用于生产环境。
PyPy、Jython、IronPython 等其它实现没有 GIL,但它们也不是 CPython。

问题 3
Python 多线程是"假的"吗?那多线程有什么意义?

💡 先说结论:不是假的,只是被 GIL 限制了

Python 的 threading真正的 OS 线程(POSIX Thread / Windows Thread), 不是绿色线程或协程模拟。它们可以被操作系统调度到不同 CPU 核心上——但问题在于: 同一时刻只有一个线程能执行 Python 字节码

🚫

CPU 密集型:多线程没用

Core 1
GIL 持有
Core 2
等待
Core 3
等待
Core 4
等待

所有核心都在等同一把锁,多线程反而因为上下文切换而更慢

I/O 密集型:多线程很有用

Core 1
线程A: I/O等待
Core 2
线程B: 运行中
Core 3
线程C: I/O等待
Core 4
线程D: 运行中

I/O 等待时会释放 GIL,其他线程可以继续执行

多线程的意义(I/O 密集型场景):

当线程做 I/O 操作(网络请求、文件读写、time.sleep())时,GIL 会被释放,此时其他线程可以获取 GIL 并执行。

所以多线程在以下场景非常有用:
  • 爬虫:同时发成百上千个 HTTP 请求
  • Web 服务器:每个请求一个线程处理(Django/Flask 的 WSGI 模式)
  • 文件 I/O:同时读写多个文件
  • 等待外部 API 响应
demo_io_vs_cpu.py — I/O 密集 vs CPU 密集
# CPU 密集型:多线程无法加速(受 GIL 限制)
def cpu_bound():
    for i in range(10**8):
        _ = i * i

# I/O 密集型:多线程可以大幅加速
def io_bound():
    import time
    time.sleep(1)   # 释放 GIL!其他线程可以运行

# 网络 I/O:requests.get() 内部是 C 实现的,
# 在等待响应时会释放 GIL
def fetch_url(url):
    import requests
    resp = requests.get(url)  # GIL 在此处释放
    return len(resp.text)
问题 4
GIL 在什么情况下会释放?(实现机制)
CPython 的 GIL 释放机制经历了多次改进,目前(Python 3.2+)使用的是超时机制 + 协作式释放

🔑 GIL 释放的三种情况

释放场景 触发条件 说明
I/O 操作 read / write / send / recv 等系统调用 在 I/O 等待前主动释放 GIL,I/O 完成后重新获取
time.sleep() 调用 time.sleep(n) 线程休眠时释放 GIL,定时器到期后重新竞争
字节码超时 默认每 5ms(Python 3.2+)或 15ms(旧版) 解释器强制当前线程释放 GIL,让其他线程有机会运行
C 扩展主动释放 C 代码中调用 Py_BEGIN_ALLOW_THREADS NumPy、Pillow 等 C 扩展在计算时释放 GIL
长时间计算 纯 Python 循环(无 I/O) ⚠️ GIL 不会释放!会一直占用直到超时
gil_mechanism.c — CPython 中 GIL 的简化逻辑(概念性)
/*
 * CPython 3.x 的 GIL 实现(ceval.c / gil.c)
 * 核心数据结构(简化):
 *
 * gil_mutex:   保护 GIL 的互斥锁
 * gil_cond:    条件变量,用于线程间通知
 * gil_timeout:  当前持有 GIL 的线程最长持有时间(~5ms)
 * drop_request: 其他线程请求当前线程释放 GIL 的标志
 */

// 线程获取 GIL 的简化逻辑
take_gil(PyThreadState *tstate) {
    wait_for_gil();   // 等待 GIL 可用
    // 获取成功,设置定时器
    set_gil_timeout(5_MS);
}

// 字节码执行主循环(ceval.c 核心)
PyEval_EvalFrameDefault(...) {
    for (;;) {
        // 检查是否该释放 GIL 了(超时或 drop_request)
        if (gil_must_exit()) {
            drop_gil();    // 释放 GIL
            take_gil(tstate); // 重新竞争 GIL
        }
        // 执行一条字节码指令
        execute_opcode(opcode);
    }
}

// C 扩展如何主动释放 GIL(NumPy 等使用此宏)
// Py_BEGIN_ALLOW_THREADS 展开为:
//   PyThreadState *_save; \
//   _save = PyEval_SaveThread();
//
// Py_END_ALLOW_THREADS 展开为:
//   PyEval_RestoreThread(_save);
问题 5
想要真正的多线程(多核并行),怎么办?
4 种主流方案,根据场景选择:
方案 原理 适用场景 优点 缺点
multiprocessing 每个进程有独立的 GIL CPU 密集型 真正多核并行,绕过 GIL 内存开销大,进程间通信复杂
concurrent.futures 封装了 multiprocessing/threading 通用 API 简洁,推荐首选 本质同上
C 扩展(Cython/CFFI) C 代码释放 GIL 后并行计算 高性能计算 极高性能,NumPy 就是这么做的 需要写 C/Cython 代码
无 GIL 的 Python Python 3.13+ --disable-gil 未来 原生支持,无需改代码 实验性,兼容性未验证

🛠️ 方案 1:multiprocessing — 最常用

true_parallel.py — 用多进程实现真正的多核并行
import multiprocessing as mp
import time

# 计算密集型任务
def heavy_compute(n):
    return sum(i * i for i in range(n))

# ---- 单进程基线 ----
def single_process():
    t0 = time.time()
    results = []
    for i in range(4):
        results.append(heavy_compute(10**7))
    print(f"单进程: {time.time() - t0:.2f}s")

# ---- 多进程(真正并行)----
def multi_process():
    t0 = time.time()
    with mp.Pool(processes=4) as pool:
        results = pool.map(
            heavy_compute,
            [10**7] * 4
        )
    print(f"多进程(4核): {time.time() - t0:.2f}s")

# 输出(4核机器上):
# 单进程: ~3.2s
# 多进程: ~0.9s  ← 接近 4x 加速!

if __name__ == "__main__":
    # Windows 必须加这个保护!
    # 否则子进程会递归 import,导致无限 fork
    single_process()
    multi_process()

🛠️ 方案 2:concurrent.futures — 推荐首选

futures_demo.py — 更现代的 API
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
import time

def compute(x):
    return sum(i*i for i in range(x))

# CPU 密集型 → ProcessPoolExecutor(多进程,绕过 GIL)
with ProcessPoolExecutor(max_workers=4) as exe:
    results = list(exe.map(compute, [10**7] * 4))

# I/O 密集型 → ThreadPoolExecutor(轻量,受 GIL 但 I/O 时会释放)
import requests

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

with ThreadPoolExecutor(max_workers=20) as exe:
    urls = ["https://www.baidu.com"] * 20
    results = list(exe.map(fetch, urls))

🛠️ 方案 3:Cython/nogil — 高级方案

nogil_cython.pyx — Cython 中释放 GIL
# 在安装 Cython 后,可以用 nogil 块释放 GIL
# 编译后,这段 C 代码可以真正并行

import numpy as np
cimport numpy as np
from cython import nogil, parallel

def parallel_sum(np.ndarray[np.float64_t, ndim=1] arr):
    cdef double total = 0.0
    cdef int i, n = len(arr)

    # nogil: 在 C 层面释放 GIL,真正并行!
    with nogil, parallel.prange(n, schedule='static') as i:
        for i in range(n):
            total += arr[i]

    return total
问题 6
GIL 在 CPython 源码中是怎么实现的?
以 CPython 3.x 为例,GIL 的核心实现在 Python/gil.cPython/ceval.c 中。

📂 关键源码文件

CPython 源码结构(GIL 相关)
CPython 源码根目录/
├── Python/
│   ├── gil.c          ← GIL 核心实现(获取/释放/超时逻辑)
│   ├── ceval.c        ← 字节码执行主循环(解释器核心)
│   ├── thread_pthread.h  ← POSIX 线程实现(Linux/macOS)
│   └── thread_nt.h      ← Windows 线程实现
├── Include/
│   └── cpython/
│       └── pyatomic.h  ← 原子操作(用于无锁计数)
└── Modules/
    └── _threadmodule.c  ← threading 模块的 C 实现

⚙️ GIL 的核心数据结构(简化)

gil.c — GIL 核心结构(概念性 C 代码)
// CPython 3.12+ 的 GIL 实现(简化版)
// 实际源码:https://github.com/python/cpython/blob/main/Python/gil.c

// 每个线程的状态
typedef struct {
    int gil_held;       // 是否持有 GIL
    int gil_locked;     // GIL 锁计数(可重入)
    double last_tick;   // 上次获取 GIL 的时间戳
} PyThreadState;

// GIL 全局状态
typedef struct {
    pthread_mutex_t mutex;   // POSIX 互斥锁
    pthread_cond_t  cond;    // 条件变量(用于线程等待/唤醒)
    int pending_events;       // 待处理事件计数
    int gil_drop_request;     // 其他线程请求释放 GIL
    struct timespec last_switch; // 上次切换时间
} GILState;

// ==========================================
// 获取 GIL(线程启动时或恢复执行时调用)
// ==========================================
void take_gil(PyThreadState *tstate) {
    // 1. 加锁(原子操作)
    pthread_mutex_lock(&gil.mutex);

    // 2. 等待 GIL 可用(自旋 + 条件变量)
    while (gil.gil_held) {
        // 等待 gil_cond 信号(由 drop_gil 发出)
        pthread_cond_wait(&gil.cond, &gil.mutex);
    }

    // 3. 标记自己持有 GIL
    gil.gil_held = 1;
    tstate->gil_held = 1;

    // 4. 记录时间戳(用于超时判断)
    clock_gettime(CLOCK_MONOTONIC, &gil.last_switch);

    pthread_mutex_unlock(&gil.mutex);
}

// ==========================================
// 释放 GIL(I/O 前、超时、sleep 时调用)
// ==========================================
void drop_gil(PyThreadState *tstate) {
    pthread_mutex_lock(&gil.mutex);

    // 1. 标记 GIL 空闲
    gil.gil_held = 0;
    tstate->gil_held = 0;

    // 2. 唤醒一个等待 GIL 的线程
    pthread_cond_signal(&gil.cond);

    pthread_mutex_unlock(&gil.mutex);
}

// ==========================================
// 检查是否该释放 GIL(在 ceval.c 的字节码循环中调用)
// ==========================================
int _gil_check(PyThreadState *tstate) {
    struct timespec now;
    clock_gettime(CLOCK_MONOTONIC, &now);

    // 计算持有 GIL 的时间
    double held_ms = (now.tv_sec - gil.last_switch.tv_sec) * 1000.0
                   + (now.tv_nsec - gil.last_switch.tv_nsec) / 1e6;

    // 超过 5ms,请求释放(Python 3.2+ 的改进版 GIL)
    if (held_ms > 5.0) {
        gil.gil_drop_request = 1;
        return 1;  // 该释放了
    }
    return 0;
}
💡 注意:Python 3.2 引入的"新 GIL"(由 Antoine Pitrou 实现)改用条件变量 + 超时机制,解决了旧 GIL 中"持有 GIL 的线程永远不会释放,导致其他线程饿死"的问题。核心改进:用 pthread_cond_timedwait 让等待线程可以在超时后强制请求 GIL。

📌 一句话总结

  • GIL 是什么?CPython 中的一把全局互斥锁,保证同一时刻只有一个线程执行字节码
  • 为什么存在?CPython 的引用计数内存管理不是线程安全的,GIL 是简单有效的解决方案
  • Python 多线程是假的吗?不是假的,是真正的 OS 线程,但受 GIL 限制无法在多核上并行执行 Python 字节码
  • 多线程有什么意义?I/O 密集型场景中,I/O 等待时会释放 GIL,多线程可以大幅提升吞吐量
  • 什么时候释放 GIL?I/O 操作、time.sleep()、C 扩展主动释放、字节码执行超时(~5ms)
  • 怎么真正并行?CPU 密集型用 multiprocessing / ProcessPoolExecutor(每个进程独立 GIL);或用 C 扩展释放 GIL;或等 Python 3.13+ 的 no-GIL 模式成熟
场景 推荐方案 原因
CPU 密集型(计算、图像处理) multiprocessing 每个进程独立 GIL,真正多核并行
I/O 密集型(网络、文件) threading / asyncio I/O 时释放 GIL,单进程足够
高性能计算(NumPy 级别) C 扩展 / Cython nogil C 层面释放 GIL,真正并行计算
混合场景 concurrent.futures 统一 API,按需选择 ProcessPool 或 ThreadPool