Python 基础体系 · 第 36/112 篇。示例统一以 Python 3.14 为语言基线;第三方库使用与其兼容的现代稳定版本,版本敏感行为会单独说明。

Python GIL 与自由线程构建:互斥边界、扩展兼容和并行选择

先区分三个容易混淆的概念

并发程序中至少有三个不同问题:

  1. 并发(concurrency):多个任务在时间上交错推进。
  2. 并行(parallelism):多个任务在同一时刻使用不同 CPU 核心执行。
  3. 互斥(mutual exclusion):同一段临界区在同一时刻只允许一个执行者进入。

GIL 主要影响 CPython 中线程执行 Python 代码的方式;它不是通用的业务锁,也不是“所有线程操作都安全”的证明。

在默认的 GIL 构建中,某一时刻通常只有一个线程持有 GIL 并执行 Python 字节码。线程可以并发等待 I/O,但 CPU 密集型 Python 字节码不会因为创建多个线程就自动获得多核并行。Python 3.14 已正式支持可关闭 GIL 的 free-threaded 构建,但该构建仍然是可选的,不是默认解释器。(docs.python.org)

可以把关系抽象为:

线程并发
├── 等待 I/O 时交错推进
├── GIL 构建:Python 字节码通常不能多核并行
└── free-threaded 构建:Python 字节码可以多核并行,但共享状态仍需同步

进程并行
└── 每个进程拥有独立解释器和地址空间,天然绕开单个进程内的 GIL

因此,“线程能不能并发”和“线程能不能并行”不是同一个问题;“某个操作当前看起来没有出错”和“代码满足线程安全条件”也不是同一个问题。


GIL 到底保护什么

GIL 的对象级边界

GIL 是 CPython 解释器内部的全局解释器锁。默认构建中,线程在访问 Python 对象或调用 Python C API 前,需要持有 GIL。

例如,Python 对象通常包含引用计数。假设两个线程同时执行:

线程 A:读取 refcount = 10
线程 B:读取 refcount = 10
线程 A:写回 11
线程 B:写回 11

理想结果应为 12,但如果引用计数更新不是受保护的原子操作,结果可能丢失一次递增。GIL 的一个重要作用,就是在传统 CPython 中为这类解释器内部对象操作提供串行化边界。(docs.python.org)

但这个边界只说明:

解释器在处理 Python 对象内部状态时,需要避免多个线程同时破坏解释器自身的数据结构。

它不说明:

由多个字节码组成的业务操作具有事务性。

例如:

counter += 1

从业务角度看,这是一个“读取—计算—写回”的读改写操作:

old = counter
new = old + 1
counter = new

即使每一个底层对象操作都没有破坏解释器,整个读改写过程仍可能被另一个线程插入。

GIL 不等于业务互斥锁

下面这个例子展示了共享状态竞态:

# race.py
from __future__ import annotations

import threading
import time

counter = 0


def worker(increments: int) -> None:
    global counter

    for _ in range(increments):
        # 读取共享状态
        old = counter

        # 让出执行机会,放大竞态窗口
        time.sleep(0)

        # 根据旧值写回
        counter = old + 1


threads = [
    threading.Thread(target=worker, args=(10_000,))
    for _ in range(4)
]

for thread in threads:
    thread.start()

for thread in threads:
    thread.join()

print("expected:", 40_000)
print("actual:  ", counter)

预期值是:

4×10000=400004 \times 10\,000 = 40\,000

其中:

  • 4 是线程数;
  • 10_000 是每个线程的递增次数;
  • counter 是所有线程共享的变量。

一次典型的错误执行顺序如下:

初始 counter = 0

线程 A:读取 old = 0
线程 B:读取 old = 0
线程 A:写入 counter = 1
线程 B:写入 counter = 1

两次递增只留下了一个结果。

该程序的最终值通常小于 40_000,但具体数值不固定。time.sleep(0) 不是解决方案,而是人为扩大线程切换窗口;真实程序中的 I/O、锁等待、解释器调度和 C 扩展调用也可能产生类似的交错。

正确做法是保护完整的读改写过程:

# locked_counter.py
from __future__ import annotations

import threading

counter = 0
counter_lock = threading.Lock()


def worker(increments: int) -> None:
    global counter

    for _ in range(increments):
        with counter_lock:
            counter += 1


threads = [
    threading.Thread(target=worker, args=(10_000,))
    for _ in range(4)
]

for thread in threads:
    thread.start()

for thread in threads:
    thread.join()

print(counter)

threading.Lock 的互斥范围是:

acquire
  读取 counter
  计算 counter + 1
  写回 counter
release

锁保护的是应用定义的共享不变量,而不是某一条字节码。


字节码切换为什么会制造竞态

“一条 Python 语句”不是不可分割事务

Python 源码中的一条语句通常会被编译为多条字节码。具体字节码属于 CPython 实现细节,不能把某个版本的反汇编结果当作语言规范;但从执行模型上,可以把:

counter += 1

理解为一组逻辑步骤:

读取 counter
读取整数 1
执行加法
保存结果
写回 counter

传统 CPython 会在执行过程中定期尝试在线程之间切换。sys.setswitchinterval() 可以调整解释器尝试切换线程的时间间隔,但它是调度参数,不是同步机制。它不能把一组字节码变成原子事务,也不能修复竞态条件。CPython 文档明确指出,线程切换可能发生在字节码指令之间,因此纯 Python 代码仍然需要锁。(docs.python.org)

因此,以下推理是错误的:

有 GIL
→ 每条语句不可打断
→ counter += 1 一定安全

正确推理是:

有 GIL
→ 默认 CPython 不让多个线程同时执行受 GIL 保护的 Python 解释器操作
→ 线程仍可能在多个字节码步骤之间切换
→ 读改写过程仍可能交错
→ 需要显式同步共享不变量

为什么测试可能“看起来没问题”

如果临界区很短,线程切换恰好没有发生在读和写之间,竞态可能暂时不暴露:

线程 A:读、加、写
线程 B:读、加、写

这并不能证明代码安全,只能说明某次调度没有触发冲突。

竞态的危险特征是:

  • 失败概率受 CPU、负载和调度影响;
  • 增加日志可能改变失败概率;
  • 修改循环次数可能让问题消失或更频繁出现;
  • GIL 构建和 free-threaded 构建可能表现不同;
  • 单元测试通过不代表所有执行交错都被覆盖。

在 free-threaded 构建中,多个线程可以真正同时执行 Python 代码,因此原本只在特殊切换点暴露的问题,可能变成更高概率的并行冲突。


GIL 保证与不保证

GIL 可以提供的保证

在默认 GIL 构建中,GIL 主要提供以下解释器级效果:

  1. 避免多个线程同时执行需要 GIL 的 CPython 解释器代码;
  2. 保护部分 Python 对象和解释器内部状态;
  3. 允许 C 扩展在持有 GIL 时安全调用 Python C API;
  4. 在阻塞 I/O 等场景下,允许其他线程运行。

这些是 CPython 的实现机制,不应扩展为所有 Python 实现都必须采用相同方式。

GIL 不能提供的保证

GIL 不保证:

  • 多条字节码组成的业务操作不可交错;
  • 多个线程对同一个业务对象的复合修改没有竞态;
  • 条件检查与动作之间不会发生状态变化;
  • 不会发生死锁;
  • 不会出现活锁、饥饿或错误的锁顺序;
  • 用户自定义类的方法调用具有原子性;
  • 第三方 C 扩展内部状态天然线程安全;
  • free-threaded 构建下不需要锁。

尤其要警惕“内置容器操作看起来安全”的说法。free-threaded CPython 当前会为 dictlistset 等内置类型使用内部锁,使某些并发修改表现得类似 GIL 构建;但 Python 文档明确将其描述为当前实现行为,而不是对并发修改语义的长期保证,并建议使用 threading.Lock 等同步原语。(docs.python.org)

例如:

if key not in cache:
    cache[key] = build_value()

即使 key in cachecache[key] = ... 各自没有破坏字典,整个“检查—构造—写入”流程仍不是原子的:

线程 A:发现 key 不存在
线程 B:发现 key 不存在
线程 A:构造 value
线程 B:构造 value
线程 A:写入
线程 B:覆盖写入

如果 build_value() 有副作用,问题就不只是重复计算,还可能是重复扣费、重复发送请求或重复创建资源。


I/O 并发:线程为什么仍然有价值

阻塞 I/O 会释放执行机会

传统 CPython 在阻塞 I/O 周围通常会释放 GIL,例如文件读写或网络等待。这样,一个线程等待磁盘或网络时,其他线程可以继续执行 Python 代码。C API 中,这类操作通过分离线程状态实现。(docs.python.org)

因此,线程适合这样的任务:

线程 A:发起网络请求,等待响应
线程 B:处理另一个请求
线程 C:读取文件
线程 D:等待数据库连接

这里的收益不是“多个线程同时执行 Python 计算”,而是:

等待 I/O 的时间被其他任务利用

可以用线程池表达一组阻塞 I/O 工作:

# io_threads.py
from concurrent.futures import ThreadPoolExecutor
import time


def fetch(task_id: int) -> str:
    # 模拟阻塞网络或磁盘操作
    time.sleep(1)
    return f"task-{task_id} done"


def main() -> None:
    with ThreadPoolExecutor(max_workers=4) as executor:
        results = list(executor.map(fetch, range(4)))

    print(results)


if __name__ == "__main__":
    main()

前置条件是:任务确实会阻塞等待外部资源,并且被调用的库允许在线程中使用。ThreadPoolExecutor 会创建不超过 max_workers 个工作线程,并通过 Future 返回异步任务结果。(docs.python.org)

这个例子不应被解释为“4 个 Python 任务获得了 4 核 CPU”。每个任务的大部分时间都在等待 sleep 模拟的外部事件。

asyncio 是另一种 I/O 并发模型

asyncio 使用事件循环和 async/await 调度协程。协程在 await 处主动交出执行权,事件循环再运行其他可继续执行的任务。它通常适合大量网络连接和高层异步 I/O,而不依赖为每个连接创建操作系统线程。(docs.python.org)

# async_io.py
import asyncio


async def fetch(task_id: int) -> str:
    await asyncio.sleep(1)
    return f"task-{task_id} done"


async def main() -> None:
    results = await asyncio.gather(
        *(fetch(task_id) for task_id in range(4))
    )
    print(results)


if __name__ == "__main__":
    asyncio.run(main())

执行路径是:

asyncio.run(main())
  → 创建并运行事件循环
  → gather 创建多个任务
  → 每个任务运行到 await asyncio.sleep()
  → 事件循环切换到其他任务
  → 定时器到期后恢复任务
  → gather 收集结果
  → asyncio.run 关闭事件循环

asyncio 的单个事件循环通常仍在一个线程中串行执行 Python 代码。若协程直接调用阻塞函数,例如同步数据库驱动或长时间 CPU 计算,它会阻塞整个事件循环:

async def bad():
    time.sleep(10)  # 错误:阻塞事件循环

需要将阻塞工作移到线程或进程执行器,或者使用原生异步库:

async def better():
    await asyncio.to_thread(time.sleep, 10)

to_thread() 适合阻塞 I/O;它不能把 GIL 构建中的纯 Python CPU 计算自动变成多核并行。

Python 3.14 的 asyncio 已针对 free-threaded Python 提供一等支持,事件循环实现可以在多线程环境中安全使用;但一个事件循环上的任务仍然需要遵守其自身的协作式调度规则。free-threaded 构建解决的是解释器层面的多线程执行限制,不会把阻塞调用自动变成异步调用。(docs.python.org)


free-threaded CPython:Python 3.14 的实际状态

它是什么

free-threaded 构建是禁用 GIL 的 CPython 构建。CPython 从 Python 3.13 开始支持该模式,Python 3.14 起已获得正式支持,不再被标记为实验性;但它仍是可选构建。源码构建时可使用 --disable-gil。(docs.python.org)

检查当前解释器是否支持或启用了 free threading:

import sys
import sysconfig

print("build supports free-threading:",
      sysconfig.get_config_var("Py_GIL_DISABLED") == 1)

print("GIL enabled at runtime:",
      sys._is_gil_enabled())

两个结果含义不同:

  • Py_GIL_DISABLED == 1:当前解释器构建支持 free threading;
  • sys._is_gil_enabled():当前进程运行时 GIL 是否实际启用。

free-threaded 构建也可以通过 PYTHON_GIL 环境变量或 -X gil 选项重新启用 GIL。某些未声明支持 free threading 的 C 扩展被导入时,解释器也可能重新启用 GIL,并发出警告。(docs.python.org)

free-threaded 不等于“所有对象都自动安全”

禁用 GIL 后,CPython 需要用更细粒度的内部机制维护对象和内存管理。例如内置容器可能使用内部锁,部分对象可能被设计为 immortal,以减少多线程下引用计数竞争。

但这不会改变业务层的原子性边界:

# 仍然不是一个原子事务
if not queue:
    queue.append(create_item())

多个操作之间的关系仍必须由应用锁、条件变量、队列或更高层并发结构表达。

此外,free-threaded 构建存在额外运行时成本和限制。官方文档列出了一些当前行为,包括更高的单线程开销、更高的内存使用,以及同一迭代器被多个线程并发访问时可能出现重复或遗漏元素。(docs.python.org)

所以迁移 free-threaded 的正确问题不是:

“我的代码能不能启动?”

而是:

“我的共享状态、扩展模块、构建链和部署依赖是否都满足无 GIL 执行条件?”

C 扩展兼容:真正的迁移边界

为什么 Python 代码能运行,不代表扩展能运行

传统 C 扩展经常隐式依赖 GIL:

/* 伪代码 */
PyObject *value = PyDict_GetItem(dict, key);
/* 假定整个访问过程由 GIL 串行化 */

在 GIL 构建中,扩展作者可能没有为模块级静态状态、缓存、引用计数辅助结构或底层 C 数据结构增加额外锁。

在 free-threaded 构建中,扩展必须明确说明自己是否支持禁用 GIL。否则导入扩展可能触发警告并重新启用 GIL。(docs.python.org)

模块声明

使用多阶段初始化的扩展,可以在模块槽中声明:

static struct PyModuleDef_Slot module_slots[] = {
#if PY_VERSION_HEX >= 0x030D0000
    {Py_mod_gil, Py_MOD_GIL_NOT_USED},
#endif
    {0, NULL}
};

使用单阶段初始化的扩展,则需要在 free-threaded 构建下调用:

#ifdef Py_GIL_DISABLED
    PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
#endif

这些声明不是“自动加锁”。它们表示扩展作者已经审查并处理了无 GIL 执行所需的线程安全问题。扩展仍然需要保护自己的共享 C 状态,并正确管理线程状态和 Python C API 调用。(docs.python.org)

构建产物不是普通 wheel 的简单替换

free-threaded 构建需要专门构建扩展。其 wheel、共享库和二进制通常使用 t 后缀区分,例如 python3.14t。当前 free-threaded 构建也不支持 Limited C API 或 Stable ABI,因此依赖这些 ABI 方案的扩展不能直接把同一个产物当作 free-threaded 扩展使用,通常需要分别构建 wheel。(docs.python.org)

工程上的验证顺序应是:

解释器支持 free threading
  → 依赖解析得到对应的 free-threaded wheel
  → 导入所有 C 扩展
  → 检查是否触发重新启用 GIL
  → 并发测试共享状态和回调路径
  → 再进行性能和稳定性评估

可以在部署检查中记录:

import sys
import sysconfig

print(sys.version)
print("Py_GIL_DISABLED =",
      sysconfig.get_config_var("Py_GIL_DISABLED"))
print("GIL enabled =",
      sys._is_gil_enabled())

如果构建本来支持 free threading,但导入某个扩展后 sys._is_gil_enabled() 变为 True,那么程序已经不再处于预期的无 GIL 运行状态。此时不能仅凭“使用的是 free-threaded Python”判断线程获得了真正的 Python 多核并行。


锁、条件变量和死锁:GIL 消失后更不能省略

条件变量保护的是状态,不是通知本身

生产者—消费者模型中,条件变量必须与共享状态和锁一起使用:

from collections import deque
import threading

items = deque()
condition = threading.Condition()


def consumer() -> object:
    with condition:
        while not items:
            condition.wait()

        item = items.popleft()
        return item


def producer(item: object) -> None:
    with condition:
        items.append(item)
        condition.notify()

执行过程是:

消费者获得锁
  → 发现 items 为空
  → wait 释放锁并阻塞

生产者获得锁
  → 写入 items
  → notify 唤醒等待者
  → 释放锁

消费者被唤醒
  → 重新获得锁
  → 再次检查 items
  → 取出元素

notify() 只是唤醒信号,不负责释放锁;被唤醒的线程还必须重新获取关联锁。while 而不是 if 是必要的,因为线程恢复时,条件可能已经被其他线程改变。(docs.python.org)

死锁来自等待图中的环

考虑两个锁:

lock_a = threading.Lock()
lock_b = threading.Lock()

线程 A 按顺序获取 A → B,线程 B 按顺序获取 B → A

线程 A:持有 A,等待 B
线程 B:持有 B,等待 A

等待关系形成环:

ABAA \rightarrow B \rightarrow A

只要四个条件同时成立,死锁就可能发生:

  1. 资源互斥;
  2. 线程持有资源时继续等待;
  3. 资源不能被强制抢占;
  4. 等待关系形成环。

诊断时可以设置线程栈转储:

import faulthandler
import time

faulthandler.dump_traceback_later(
    30,
    repeat=True,
)

try:
    run_application()
finally:
    faulthandler.cancel_dump_traceback_later()

如果程序卡住,线程栈通常能显示每个线程停在何处;结合锁的获取顺序,可以还原等待环。join() 也必须纳入生命周期设计:线程启动后要么正常退出,要么有取消信号和超时策略。线程不能 join() 自己,否则会直接造成逻辑死锁;在解释器最终化阶段等待某些 daemon 线程还可能引发 PythonFinalizationError。(docs.python.org)


线程、进程、解释器池和异步任务如何选择

线程:共享内存优先,I/O 并发优先

选择线程通常意味着:

优点:
  共享内存方便
  调用同步库简单
  阻塞 I/O 可以重叠等待

代价:
  需要处理共享状态
  需要锁和条件变量
  GIL 构建下纯 Python CPU 计算不能获得多核并行

适合:

  • 阻塞网络客户端;
  • 文件和数据库 I/O;
  • 已经释放 GIL 的 C 扩展计算;
  • 需要共享少量进程内缓存的工作。

进程:隔离和 CPU 并行为优先

ProcessPoolExecutormultiprocessing.Pool 使用独立进程执行任务,可以绕开单个解释器的 GIL;代价是参数和返回值通常需要可序列化,进程间共享状态需要 IPC、共享内存或其他明确机制。(docs.python.org)

# process_pool.py
from concurrent.futures import ProcessPoolExecutor


def square(value: int) -> int:
    return value * value


def main() -> None:
    values = list(range(8))

    with ProcessPoolExecutor() as executor:
        results = list(executor.map(square, values))

    print(results)


if __name__ == "__main__":
    main()

if __name__ == "__main__" 不是装饰性写法。使用 spawnforkserver 时,子进程需要重新导入主模块;如果模块导入时就创建进程,可能反复创建子进程或直接失败。

Python 3.14 在 POSIX 上将默认启动方式从 fork 改为 forkserver,以减少多线程进程直接 fork 带来的不兼容;Windows 和 macOS 默认仍使用 spawn。依赖特定启动方式的代码应显式选择上下文。(docs.python.org)

import multiprocessing as mp


def main() -> None:
    ctx = mp.get_context("spawn")

    with ctx.Pool(2) as pool:
        print(pool.map(str, range(4)))


if __name__ == "__main__":
    main()

这里的关键风险是:

fork:
  复制父进程当时的内存和资源状态
  多线程程序中可能复制出带锁但没有对应线程的子进程

spawn:
  启动全新解释器
  初始化更清晰,但要求入口可导入、对象可序列化

forkserver:
  由单独服务器进程负责派生
  避免直接从多线程父进程 fork,但仍有上下文和资源继承约束

InterpreterPoolExecutor:同一进程中的多解释器隔离

Python 3.14 新增的 InterpreterPoolExecutor 使用线程承载多个解释器,每个工作解释器有独立的解释器状态和 GIL,因此可以实现多核并行。不同解释器之间不能直接共享可变对象,通信需要专门机制或序列化。(docs.python.org)

它与 free-threaded 构建的区别是:

free-threaded Python:
  一个解释器
  GIL 被禁用
  多线程可同时执行该解释器中的 Python 代码
  共享对象的线程安全审查更重要

InterpreterPoolExecutor:
  多个解释器
  每个解释器有自己的 GIL
  解释器之间隔离
  数据共享和通信成本更显式

如果任务需要大量共享可变对象,解释器池并不一定合适;如果任务可以被清晰划分、只交换少量数据,隔离反而会降低共享状态竞态的复杂度。

asyncio:任务数量多、I/O 等待密集

可以按执行特征作如下判断:

工作负载 优先考虑 原因
少量阻塞 I/O,已有同步库 线程 改造成本低,等待期间可切换
大量网络连接,库支持异步 asyncio 事件循环管理大量协作式任务
纯 Python CPU 密集计算,默认构建 进程或解释器池 绕开单解释器 GIL
纯 Python CPU 密集计算,free-threaded 构建 线程、进程或解释器池 线程可以利用多核,但要承担共享状态和扩展兼容成本
C 扩展已释放 GIL 的计算 线程可能合适 CPU 工作实际在 GIL 外执行
强隔离、故障域隔离、独立资源限制 进程 地址空间和生命周期独立

这不是单纯的速度排序。最终选择取决于四个量:

Ttotal=Tcompute+Twait+Ttransfer+TsynchronizationT_{\text{total}} = T_{\text{compute}} + T_{\text{wait}} + T_{\text{transfer}} + T_{\text{synchronization}}

其中:

  • TcomputeT_{\text{compute}}:实际计算时间;
  • TwaitT_{\text{wait}}:等待网络、磁盘、数据库等外部资源的时间;
  • TtransferT_{\text{transfer}}:线程、进程或解释器之间传递数据的时间;
  • TsynchronizationT_{\text{synchronization}}:锁竞争、队列等待和协调开销。

默认 GIL 构建通常无法通过线程减少纯 Python 的 TcomputeT_{\text{compute}};线程主要减少等待造成的空闲。free-threaded 构建可能减少计算部分的串行限制,但会增加同步、内存和扩展兼容方面的要求。进程减少 GIL 限制,却可能增加数据传输和进程管理成本。


一个可执行的决策实验

不要先凭直觉迁移到 free-threaded 或进程池,可以先把任务拆成三种测量:

# workload.py
from __future__ import annotations

import hashlib
import time


def cpu_task(size: int) -> str:
    data = b"x" * size
    digest = hashlib.sha256(data).hexdigest()
    return digest


def run_serial(count: int, size: int) -> float:
    start = time.perf_counter()

    for _ in range(count):
        cpu_task(size)

    return time.perf_counter() - start


if __name__ == "__main__":
    elapsed = run_serial(100, 1_000_000)
    print(f"serial: {elapsed:.3f}s")

然后分别替换执行器:

from concurrent.futures import (
    ThreadPoolExecutor,
    ProcessPoolExecutor,
)


def run_threaded(count: int, size: int) -> float:
    start = time.perf_counter()

    with ThreadPoolExecutor() as executor:
        list(executor.map(cpu_task, [size] * count))

    return time.perf_counter() - start


def run_processes(count: int, size: int) -> float:
    start = time.perf_counter()

    with ProcessPoolExecutor() as executor:
        list(executor.map(cpu_task, [size] * count))

    return time.perf_counter() - start

这个实验不能直接推出普遍性能结论,因为 hashlib 是否释放 GIL、输入大小、核心数、进程启动方式和数据传输都会影响结果。它的价值在于帮助确认当前工作负载属于哪一类:

线程接近串行:
  可能是 GIL、锁竞争或任务太小

进程明显更快:
  可能是纯 Python CPU 工作受 GIL 限制

线程也有明显并行收益:
  可能是 C 扩展释放 GIL,或运行在 free-threaded 构建

所有方案都差:
  可能是任务粒度太小,调度和传输成本主导

测量时还要记录解释器类型:

import sys
import sysconfig

print(sys.version)
print(sysconfig.get_config_var("Py_GIL_DISABLED"))
print(sys._is_gil_enabled())

否则同一份代码在默认 GIL 构建、free-threaded 构建和被扩展重新启用 GIL 的进程中,可能被误认为是同一种运行环境。


最后应保留的边界

GIL 是 CPython 解释器内部的串行化机制,不是应用层事务锁。它可以防止解释器对象管理被任意并发破坏,却不能保证跨多个字节码的业务操作具有原子性。

字节码切换解释了为什么默认 GIL 构建中仍会出现竞态;阻塞 I/O 释放执行机会解释了为什么线程对 I/O 工作仍有价值;free-threaded 构建解释了为什么线程现在可以真正利用多个 CPU 核心,也解释了为什么锁、C 扩展审查和部署验证变得更加重要。

因此,可靠的并发设计应从共享状态和任务边界出发:

没有共享可变状态
  → 优先使用协程、线程或进程中更简单的模型

有共享可变状态
  → 明确定义不变量
  → 用 Lock、Condition、Queue 等原语表达状态转换

需要纯 Python CPU 并行
  → 默认构建考虑进程或解释器池
  → free-threaded 构建考虑线程,但先验证依赖和竞态

依赖 C 扩展
  → 检查 free-threaded 声明、wheel 和运行时 GIL 状态

需要隔离和故障控制
  → 使用进程,而不是仅依赖线程锁

真正的选择不是“GIL 还在不在”,而是:任务是否等待 I/O,数据是否共享,计算是否在 Python 层完成,扩展是否支持目标构建,以及系统是否能够接受相应的同步、传输和故障成本。


系列导航与关联阅读

官方资料

本文依据 Python 官方文档、相关 PEP 与生态项目官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。