← [ BACK_TO_NOTES ]

Python GIL 与线程

从 OS Thread、CPU Core 与 CPython Interpreter 三层解释 GIL:它限制传统 CPython 同一解释器内多线程并行执行 Python bytecode,但线程仍适合阻塞 I/O,也可承载会释放 GIL 的 native computation。

#concept #growing #dev #python #python

[!info] related notes

Python GIL 与线程

一句话理解

GIL(Global Interpreter Lock)是传统 CPython 解释器中的一把全局锁:同一个 interpreter 内,通常只有一个 OS Thread 能在某一时刻执行 Python bytecode。

它不是“CPU 只能用一个核心”,也不是“Python 不能并发”。真正要分清的是:

CPU Core
→ 由 OS Scheduler 分配给 Thread / Process

OS Thread
→ 可以被调度到不同 CPU Core

GIL
→ 决定同一个 CPython interpreter 中,哪个 Thread 此刻有资格执行 Python bytecode

所以即使机器有 8 个核心、进程里也有 8 个线程,如果 8 个线程都在执行大量纯 Python bytecode,它们仍会竞争同一把 GIL。

为什么这个概念容易被误解

常见说法:

“Python 多线程是假的,因为 Python 只能单核。”

这句话太粗糙。

更准确:

OS Thread 本身可以多核调度
+
传统 CPython 同一 interpreter 的 Python bytecode 并行受 GIL 限制

也就是说:

Thread parallelism capability
!=
Python bytecode parallelism capability

如果线程进入 I/O 等待,或者进入会释放 GIL 的 C/C++/NumPy/PyTorch 等 native code,多个线程仍然可能获得非常有价值的并发甚至底层并行。

三层模型:OS Scheduler、GIL、Event Loop

这三者千万不要混在一起。

层次作用调度/约束对象
OS Scheduler抢占式分配 CPU 时间Thread / Process
GIL限制传统 CPython bytecode 执行资格同一 interpreter 内的 Thread
asyncio Event Loop协作式调度 coroutineTask / coroutine

例如一个 FastAPI 进程:

Python Process
├─ Thread A
│  └─ Event Loop
│     ├─ Task 1
│     ├─ Task 2
│     └─ Task 3
│
└─ Thread B
   └─ blocking / native work
  • Task 1/2/3 由 Event Loop 在 await suspend point 协作式切换。
  • Thread A/B 由 OS 抢占式调度。
  • 如果 A/B 都在执行普通 Python bytecode,还要竞争同一个 GIL。

因此:

Event Loop scheduling
!= OS thread scheduling
!= GIL competition

纯 Python CPU-heavy 多线程为什么通常不加速

例如:

def crunch(n: int) -> int:
    total = 0
    for i in range(n):
        total += i * i
    return total

如果开两个 Thread 同时跑:

Thread A ─┐
          ├─ one CPython interpreter + one GIL
Thread B ─┘

它们更像:

A: █████      █████      █████
B:      █████      █████

而不是:

A: █████████████████
B: █████████████████
   ↑ 两份 Python bytecode 真正同时执行

因此总 CPU 工作并没有因为“线程更多”而消失,反而还有:

  • GIL competition;
  • context switching;
  • cache disturbance;

等额外开销。

GIL 是怎么被释放/切换的

可以先记三类情况。

1. 纯 Python 长时间执行:周期性给其他线程竞争机会

一个线程不会永久霸占 GIL。解释器会周期性产生线程切换机会,让其他等待线程尝试获得 GIL。

这让多个 Python Thread 可以“交替前进”,但仍不等于同时并行执行多份 Python bytecode。

2. Blocking I/O:等待时释放 GIL

当线程进行文件、socket 等阻塞 I/O 时,它大部分时间其实没有 Python 计算可做。

许多 CPython I/O 路径会在等待期间释放 GIL:

Thread A
→ 发起 blocking I/O
→ release GIL
→ OS 等 I/O

Thread B
→ acquire GIL
→ 继续执行 Python

所以线程对 blocking I/O 仍然非常有价值。

3. Native Extension 主动释放 GIL

C/C++ extension 如果进入一段不需要操作 Python 对象的耗时工作,可以主动释放 GIL。

因此:

Python call
→ NumPy / PyTorch / BLAS / CUDA
→ native computation

可能在 native 层使用多核或 GPU,而不是被“Python 线程”这几个字限制成单核。

这也是为什么 SentenceTransformer / PyTorch Embedding 的实际并行能力必须 benchmark,而不能只靠 GIL 直觉判断。

为什么线程对 I/O-bound 很适合

假设五个同步网络请求,每个都需要等待 200ms。

单线程串行:

A wait 200
B wait 200
C wait 200
D wait 200
E wait 200
≈ 1s

多个 Thread 可以让等待重叠:

T1 → request A → wait
T2 → request B → wait
T3 → request C → wait
...

因为等待 I/O 时通常不需要持续持有 GIL,所以总 wall-clock 可以接近最慢一批 I/O,而不是简单相加。

最小示例

from concurrent.futures import ThreadPoolExecutor, as_completed
from time import perf_counter, sleep


def fetch_meta(item_id: str) -> str:
    sleep(0.2)  # 模拟 blocking I/O
    return f"meta:{item_id}"


def main() -> None:
    ids = [f"id-{i}" for i in range(5)]
    t0 = perf_counter()
    with ThreadPoolExecutor(max_workers=5) as pool:
        futures = [pool.submit(fetch_meta, i) for i in ids]
        results = [f.result() for f in as_completed(futures)]
    print(len(results), round(perf_counter() - t0, 2))


if __name__ == "__main__":
    main()

关键收益是等待重叠,不是五个线程一起执行五份 Python bytecode。

GIL 与 asyncio.to_thread() 的关系

result = await asyncio.to_thread(blocking_fn)

会把 blocking_fn 放到 Worker Thread,让 Event Loop 所在线程可以继续调度其他 coroutine。

如果 blocking_fn 是纯 Python CPU-heavy:

Event Loop Thread
        ↕
       GIL
        ↕
Worker Thread running heavy Python

那么 to_thread() 的主要收益是:

保护 Event Loop responsiveness。

它不保证 blocking_fn 自身变快或事件循环满足硬延迟,因为 Worker Thread 仍要竞争 GIL;长时间持有 GIL 的原生调用也可能卡住其他 Python 线程。取消 to_thread 的等待不会强制终止已经运行的工作线程。

因此:

Event Loop responsiveness ↑
!= CPU task latency ↓

详见 Executor、Thread、Process 与 asyncio.to_thread。

Thread 与 Process 的关键差别

Thread

same process
same memory
same interpreter
traditional CPython: same GIL

优点:

  • 共享对象方便;
  • 创建/切换较轻;
  • 适合 blocking I/O / native library。

代价:

  • shared mutable state 复杂;
  • 纯 Python CPU parallelism 受 GIL 限制。

Process

Process A → interpreter A → GIL A
Process B → interpreter B → GIL B

它们可以分别被 OS 调度到不同核心,真正并行执行多份 Python bytecode。

代价:

  • 独立 memory;
  • IPC / pickle;
  • 启动更重;
  • 大模型可能复制多份。

见 Python 多进程与进程池。

Thread Safety:共享内存带来的另一面

线程共享同一个进程内存,因此两个 Thread 可能同时看到同一个:

shared_list = []
shared_model = ...
shared_cache = {}

这很方便,但也意味着必须思考:

谁拥有这个 mutable state?
能否并发访问?
是否需要 Lock / Queue / Semaphore?
库本身 thread-safe 吗?

例如 BodySense 如果多个 Worker Thread 同时调用一个共享 SentenceTransformer 实例,就必须确认模型调用的线程安全与底层资源竞争,而不能只看到 to_thread() 能运行。

Free-threaded CPython 是一个重要边界

CPython 3.13 起提供可选 free-threaded 构建,3.14 成为正式支持的可选构建;它可禁用 GIL,但运行时配置或不兼容扩展仍可能启用 GIL,不能只看 Python 版本号。

但在学习和审查常规生产 Python 服务时,仍应先确认实际 interpreter/build/runtime,再决定能否使用“无 GIL”心智。

审查 BodySense L4 时,若尚无当前运行时证据,可先采用以下保守分析假设;这不是对当前部署构建的已验证结论:

GIL-enabled CPython
+ asyncio Event Loop
+ explicit thread/process/native boundaries

而不是假设所有部署都已经 free-threaded。

常见误区

[!warning] “GIL = Python 只有一个线程” 错。Python 进程完全可以有多个真实 OS Thread;GIL 约束的是同一传统 CPython interpreter 中的 Python bytecode 并行执行。

[!warning] “有 8 核,所以 8 个 Python Thread 一定能同时执行纯 Python” 错。OS 可以把线程放到不同核心,但它们仍要争 GIL。

[!warning] “线程对 Python 没用” 错。blocking I/O、同步 SDK、会释放 GIL 的 native library 都是线程的重要使用场景。

[!warning] “to_thread = CPU 加速” 错。它首先是 blocking-work isolation / Event Loop protection。

[!warning] “GIL 与 Event Loop 是同一套调度机制” 错。Event Loop 调 coroutine,OS 调 Thread,GIL 只控制 interpreter bytecode 执行资格。

工程选择速查

工作负载常见优先方案
原生 async HTTP / DBasyncio native async
只有 sync API 的 blocking I/OThreadPool / asyncio.to_thread()
会释放 GIL 的同步 native computebounded Thread + benchmark
重纯 Python CPUProcessPool / worker
很轻的本地 CPUinline,别过度 offload

自测题

  1. OS Thread 能否被调度到不同 CPU Core?
  2. GIL 限制的是 CPU Core,还是 Python bytecode 执行资格?
  3. Event Loop、OS Scheduler、GIL 分别负责什么?
  4. 为什么 blocking I/O 中线程仍然有效?
  5. GIL 常在哪三类情况下释放/切换?
  6. 为什么 to_thread() 可以保护 Event Loop,却不保证纯 Python CPU 任务更快?
  7. 为什么 PyTorch/NumPy 不能简单按“Python Thread 单核”判断?
  8. 为什么 heavy pure Python 常更适合 ProcessPool?

本节总结

最准确的心智不是“Python 单线程”,而是:

OS 可以多线程、多核调度
+
传统 CPython 同一 interpreter 的 Python bytecode 受 GIL 限制
+
I/O / native code 可以释放 GIL
+
asyncio Event Loop 又是另一层 coroutine scheduler

理解这四层后,Thread、Process、Executor 和 asyncio 的选型就不再依赖模糊口号。

创建于 2026/7/15 更新于 2026/10/6
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本