Python 基础体系 · 第 36/112 篇。示例统一以 Python 3.14 为语言基线;第三方库使用与其兼容的现代稳定版本,版本敏感行为会单独说明。
Python GIL 与自由线程构建:互斥边界、扩展兼容和并行选择
先区分三个容易混淆的概念
并发程序中至少有三个不同问题:
- 并发(concurrency):多个任务在时间上交错推进。
- 并行(parallelism):多个任务在同一时刻使用不同 CPU 核心执行。
- 互斥(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是线程数;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 主要提供以下解释器级效果:
- 避免多个线程同时执行需要 GIL 的 CPython 解释器代码;
- 保护部分 Python 对象和解释器内部状态;
- 允许 C 扩展在持有 GIL 时安全调用 Python C API;
- 在阻塞 I/O 等场景下,允许其他线程运行。
这些是 CPython 的实现机制,不应扩展为所有 Python 实现都必须采用相同方式。
GIL 不能提供的保证
GIL 不保证:
- 多条字节码组成的业务操作不可交错;
- 多个线程对同一个业务对象的复合修改没有竞态;
- 条件检查与动作之间不会发生状态变化;
- 不会发生死锁;
- 不会出现活锁、饥饿或错误的锁顺序;
- 用户自定义类的方法调用具有原子性;
- 第三方 C 扩展内部状态天然线程安全;
- free-threaded 构建下不需要锁。
尤其要警惕“内置容器操作看起来安全”的说法。free-threaded CPython 当前会为 dict、list、set 等内置类型使用内部锁,使某些并发修改表现得类似 GIL 构建;但 Python 文档明确将其描述为当前实现行为,而不是对并发修改语义的长期保证,并建议使用 threading.Lock 等同步原语。(docs.python.org)
例如:
if key not in cache:
cache[key] = build_value()
即使 key in cache 和 cache[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
等待关系形成环:
只要四个条件同时成立,死锁就可能发生:
- 资源互斥;
- 线程持有资源时继续等待;
- 资源不能被强制抢占;
- 等待关系形成环。
诊断时可以设置线程栈转储:
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 并行为优先
ProcessPoolExecutor 或 multiprocessing.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__" 不是装饰性写法。使用 spawn 或 forkserver 时,子进程需要重新导入主模块;如果模块导入时就创建进程,可能反复创建子进程或直接失败。
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 外执行 |
| 强隔离、故障域隔离、独立资源限制 | 进程 | 地址空间和生命周期独立 |
这不是单纯的速度排序。最终选择取决于四个量:
其中:
- :实际计算时间;
- :等待网络、磁盘、数据库等外部资源的时间;
- :线程、进程或解释器之间传递数据的时间;
- :锁竞争、队列等待和协调开销。
默认 GIL 构建通常无法通过线程减少纯 Python 的 ;线程主要减少等待造成的空闲。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 完整学习路线:从语言模型、并发到 Web、数据、AI 与生产交付
- 上一篇:Python weakref 与 slots:生命周期观察、对象布局和内存取舍
- 下一篇:Python 文件系统与 pathlib:路径、遍历、元数据、原子替换和竞态
- 延伸:Python 线程:生命周期、锁、条件变量、竞态和死锁诊断
- 延伸:Python 多进程:启动方式、IPC、共享内存、Pool 与回收
官方资料
本文依据 Python 官方文档、相关 PEP 与生态项目官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。

评论
0 条讨论