Chapter 47: Subinterpreters (PEP 684)
本章要建立的判断能力是:看到“用 subinterpreter 获得并行”这类说法时,能追踪它落在同一进程内的哪些 runtime state 上,能区分 PyInterpreterState、PyThreadState、GIL、模块状态、扩展模块状态和对象所有权的边界,并能判断一个任务是否适合用 subinterpreter 承载。
贯穿本章的例子是一类常见工程需求:同一个 Python 服务需要加载多个插件,每个插件都有自己的依赖、模块级缓存和 CPU 密集计算。使用线程时,插件之间共享 sys.modules、全局对象和进程资源;使用进程时,隔离强,但启动、内存和通信成本更高。subinterpreter 位于二者之间:它仍在同一进程内运行,却为 Python 运行时状态提供解释器级隔离。
PEP 684 的核心变化是把“每个解释器可以拥有自己的 GIL”推进为 CPython 3.12 之后的运行时能力。官方 PEP 684 把目标描述为 per-interpreter GIL;Python 3.14 文档进一步提供了面向 Python 代码的 concurrent.interpreters 和 InterpreterPoolExecutor。本章默认讨论 CPython;PyPy、MicroPython 等实现的解释器结构和并行约束需要分别确认。
这章的主线可以压缩成一句话:subinterpreter 的并行能力来自“同一进程内多个独立解释器状态 + 可选的 per-interpreter GIL”,它的成本来自“对象跨解释器传递必须遵守安全规则 + 扩展模块需要显式支持隔离”。
47.1 subinterpreter
subinterpreter 是同一 CPython 进程内的第二个、第三个或更多解释器实例。这里的“解释器”指一组 Python runtime state:模块表、builtins、导入状态、异常状态、线程状态列表、GC 状态以及一批解释器级配置。主解释器是进程初始化时创建的第一个解释器;subinterpreter 由运行时 API 创建,并和主解释器同处一个 OS process。
把贯穿例子放进这个模型:服务进程启动后,主解释器负责接受请求和调度插件;每个插件可以在自己的 subinterpreter 中导入模块、初始化缓存、执行代码。插件 A 在自己的解释器中 import settings,插件 B 在另一个解释器中也 import settings,两者看到的是各自解释器里的模块对象和模块字典。它们仍共享进程的地址空间、文件描述符、环境变量和底层 OS 资源,因此隔离是 Python runtime state 层面的隔离,安全边界仍需交给进程、容器或系统权限模型。
下面的 Python 3.14 示例展示了“解释器对象”和“运行上下文”的关系。它用 concurrent.interpreters 创建解释器,并在该解释器的 __main__ namespace 中执行代码。这个示例服务于语义观察:代码切换到另一个解释器执行,模块级名字绑定留在那个解释器的状态中。
from concurrent import interpreters
plugin_interp = interpreters.create()
plugin_interp.exec("""
plugin_name = "image_filter"
cache = {"loaded": True}
print(plugin_name, cache)
""")
print("main interpreter continues here")
这段代码的关键点是:plugin_name 和 cache 绑定在目标解释器的 __main__ 模块中,主解释器不会因此得到同名绑定。exec() 在目标解释器中使用它自己的 frame、globals、builtins 和模块状态。subinterpreter 因此适合表达“同一进程内多个独立 Python 执行环境”。
从 runtime 关系看,subinterpreter 并非线程的别名。线程是 OS 调度实体,在 CPython 中通过 PyThreadState 挂接到某个解释器;subinterpreter 是 PyInterpreterState 代表的运行时状态集合。一个线程可以切换当前附着的解释器,多个线程也可以分别附着到不同解释器。并行能力需要线程参与调度,而隔离能力来自解释器状态分离。
这张图的边界是 CPython runtime 层级:一个进程包含多个解释器状态,每个解释器拥有自己的线程状态链和模块表。图中没有画文件描述符、环境变量和底层内存分配器,因为这些对象属于更低层的进程资源,subinterpreter 只对其中一部分做运行时约束。
判断一个对象是否属于 subinterpreter 模型,应先问三个问题:它是否是 Python runtime state,它是否可按解释器拆分,它是否会通过 C 全局变量或进程资源绕过解释器边界。模块字典、sys.modules、builtins 和当前线程状态属于解释器模型;socket fd、进程环境变量和同一地址空间内的 native 全局状态需要额外约束。
47.2 per-interpreter GIL
per-interpreter GIL 是 PEP 684 的核心:每个足够隔离的解释器可以拥有自己的 Global Interpreter Lock。GIL 是 CPython 用来保护解释器执行和大量 runtime state 的互斥锁。传统 CPython 中,多个解释器曾共享一个进程级 GIL;当多个解释器拥有各自 GIL 时,解释器 A 的 Python 字节码执行不再被解释器 B 的 Python 字节码执行统一串行化。
把插件例子继续推进:插件 A 和插件 B 都做 CPU 密集计算。如果它们只是主解释器中的两个线程,常规 CPython 的共享 GIL 会让同一时刻只有一个线程执行 Python bytecode。若插件 A、B 分别运行在拥有自己 GIL 的解释器中,并由不同 OS 线程承载,它们的 Python bytecode 可以在不同 CPU core 上同时推进。
Python 3.14 的 InterpreterPoolExecutor 把这个模型包装成 familiar executor 接口。下面的代码展示的是用户可见层面的并行入口;它背后的关键状态是“每个 worker 线程绑定自己的 interpreter”。
from concurrent.futures import InterpreterPoolExecutor
def count_primes(limit: int) -> int:
total = 0
for candidate in range(2, limit):
for factor in range(2, int(candidate ** 0.5) + 1):
if candidate % factor == 0:
break
else:
total += 1
return total
limits = [80_000, 82_000, 84_000, 86_000]
with InterpreterPoolExecutor(max_workers=4) as pool:
results = list(pool.map(count_primes, limits))
print(results)
这段代码能说明三层关系。第一,InterpreterPoolExecutor 仍然使用线程承载 worker,因此 OS 可以把 worker 放到不同 CPU core。第二,每个 worker 的解释器拥有自己的 GIL,因此 Python bytecode 的执行不被一个进程级 GIL 汇聚到同一把锁。第三,函数、参数和返回值需要在解释器之间传递,Python 3.14 文档说明该 executor 会对 callable、参数和返回值执行序列化;所以并行收益要覆盖序列化、传输和解释器管理成本。
PEP 684 在 C API 层通过 PyInterpreterConfig 表达配置。下面是简化后的 C 形状,展示 gil 配置和隔离配置如何一起出现。它是示意代码,用于理解字段关系,真实工程需要按目标 Python 版本的头文件和错误处理规范编写。
PyInterpreterConfig config = {
.use_main_obmalloc = 0,
.allow_fork = 0,
.allow_exec = 0,
.allow_threads = 1,
.allow_daemon_threads = 0,
.check_multi_interp_extensions = 1,
.gil = PyInterpreterConfig_OWN_GIL,
};
PyThreadState *tstate = NULL;
PyStatus status = Py_NewInterpreterFromConfig(&tstate, &config);
这组字段传达了一个判断:own GIL 依赖隔离配置共同成立。解释器拥有自己的 GIL 后,普通 Python 对象共享会暴露引用计数、内部可变状态和扩展模块全局状态的并发问题。CPython 文档要求在这种模式下谨慎保存隔离,原因正是 GIL 的保护范围已经从“进程内所有解释器”收窄为“单个解释器”。
所以 per-interpreter GIL 的结论应写成运行时边界:它提升的是多个解释器之间执行 Python code 的并行潜力;它没有把单个解释器内部的多个线程变成无锁并行;它也没有自动让任意对象和扩展模块跨解释器共享变得安全。
47.3 interpreter isolation
interpreter isolation 是 subinterpreter 能成立的前提。隔离的核心对象包括 module state、thread state、object ownership、extension module state 和 cross-interpreter data。只要其中一个对象仍在解释器之间隐式共享,per-interpreter GIL 就可能把原来被共享 GIL 掩盖的数据竞争暴露出来。
先看 module state。每个解释器都有自己的 sys.modules,同一 Python 模块在不同解释器中导入时会形成不同的 module object。对于纯 Python 模块,这通常意味着模块级变量、函数对象、类对象和缓存字典按解释器分离。插件 A 修改 settings.DEBUG,插件 B 的 settings 模块对象不会自动同步。这个特性适合插件隔离、测试运行器和嵌入式脚本环境。
再看 thread state。PyThreadState 保存当前线程在某个解释器中的执行状态,例如当前异常、frame 执行相关状态和递归深度等。一个线程调用目标解释器执行函数时,运行时必须让当前线程附着到目标解释器的 thread state。调度错误会让代码在错误的解释器上下文中读写模块表、异常状态或 import state。
object ownership 是最容易误判的部分。一个普通 Python 对象属于创建它的解释器上下文。对象头部的引用计数、类型指针、GC tracking 和内部可变字段都假设运行时按特定规则维护。把一个 list 或用户自定义对象直接塞进另一个解释器,会让两个解释器在各自 GIL 下访问同一块对象状态。此时每把 GIL 只保护自己的解释器,无法保护共享对象本身。
下面的示例用伪代码表达错误边界。它是边界示意代码,只用来说明“引用跨界”和“数据跨界”的差异。
# Pseudocode for the boundary, not a real public API.
shared_list = ["task-a", "task-b"]
# Problematic shape: another interpreter receives the same list object reference.
run_in_other_interpreter(shared_list)
# Stable shape: another interpreter receives serialized data or a supported transfer object.
payload = pickle.dumps(shared_list)
run_in_other_interpreter(payload)
上面的差异是本章最关键的隔离判断。传递同一个对象引用会让所有权和同步责任变得模糊;传递序列化字节、不可变数据副本或 runtime 明确支持的 cross-interpreter object,可以把状态边界固定下来。Python 3.14 的 concurrent.interpreters 文档把多数对象传递描述为 pickle 复制,也提供 cross-interpreter queue;这类 API 的价值在于让传递行为成为显式动作。
extension module state 需要单独看。多阶段初始化的扩展模块可以为每个解释器创建独立 module object 和 per-module state;单阶段初始化或大量使用 C 全局变量的扩展可能把状态漏到解释器边界之外。PEP 684 因此要求 import system 能限制不兼容扩展模块,C 扩展维护者也需要声明并验证多解释器兼容性。
这张图说明了导入路径的隔离检查顺序。纯 Python 模块主要关注解释器级模块表;多阶段 C extension 关注 per-module state;单阶段扩展和 C 全局变量关注跨解释器共享风险。一个插件系统选择 subinterpreter 时,依赖包的 C 扩展兼容性会直接影响方案可行性。
47.4 parallel execution
parallel execution 的收益取决于四个条件:解释器隔离是否充分,数据传递是否低成本,扩展模块是否兼容,任务粒度是否大到覆盖调度和序列化开销。满足这些条件时,subinterpreter 可以在同一进程内提供接近进程池的多核执行能力,同时保留更轻的进程内调度和内存模型。
继续看插件服务。适合 subinterpreter 的任务通常具有“计算主体独立、输入输出边界清晰、依赖可在各解释器内初始化”的形状。例如图片批处理、独立规则引擎、CPU 密集解析器、独立模板渲染、独立特征提取。每个任务接收 bytes、str、int、tuple 或可 pickle 的结构,计算完成后返回结果。中间状态留在 worker 解释器里,任务之间通过消息传递协作。
不适合的任务形状也很明确:大量共享可变对象、频繁细粒度调用、依赖全局 native 状态、需要同一个连接池或同一个大型 Python 对象图被多个 worker 原地修改。此时 subinterpreter 会把“共享状态”改写成“复制、序列化、队列和同步协议”,总成本可能超过并行收益。
可以用下面的检查顺序评估收益:先估算单个任务的 CPU 时间,再估算输入输出序列化和复制成本;先确认依赖包是否支持多解释器,再确认任务是否依赖进程级全局资源;先确认异常和取消如何回到调用方,再确认 worker 生命周期和缓存是否会造成内存峰值。
# Shape that usually fits interpreters: clear data in, clear data out.
def render_tile(tile_bytes: bytes, profile_name: str) -> bytes:
profile = load_profile(profile_name) # initialized inside each interpreter
pixels = decode(tile_bytes) # local object graph
result = apply_profile(pixels, profile) # CPU-bound local work
return encode(result) # bytes cross the boundary
这段代码的可迁移判断在于数据边界。输入是 bytes 和 str,输出是 bytes;中间的 profile、pixels 和 result 都属于当前 worker 解释器。即使每个 worker 各自加载一份 profile 会增加内存,隔离边界也清楚,调试时可以把问题定位到某个 worker 的解释器状态。
和 ProcessPoolExecutor 相比,InterpreterPoolExecutor 的隔离层级较低。进程池拥有 OS 进程边界,崩溃隔离、地址空间隔离和信号边界更强;subinterpreter 共享同一进程,native 崩溃会影响整个进程。和 ThreadPoolExecutor 相比,subinterpreter 的共享少、传递显式、多核 Python bytecode 潜力更强;代价是对象共享和第三方扩展兼容性需要额外设计。
工程上可以把三类 executor 放到同一组维度比较:线程池适合 I/O overlap 和共享内存协作;解释器池适合独立 CPU 任务和显式数据传递;进程池适合强隔离、故障边界和 native 依赖复杂的 CPU 任务。这个比较维度比“哪个更快”更稳定,因为真实性能取决于任务粒度、数据大小、依赖形态和平台实现。
47.5 runtime separation
runtime separation 要回答一个源码阅读问题:哪些状态保存在 _PyRuntimeState,哪些状态保存在 PyInterpreterState,哪些状态保存在 PyThreadState。只有把三层状态分清,才能理解为什么 PEP 684 需要多年整理全局状态,也才能判断某个 bug 是进程级共享、解释器级污染,还是线程级执行状态错误。
_PyRuntimeState 可以理解为 CPython 进程级 runtime 容器。它承载 runtime 初始化、全局解释器链表、底层锁、allocator 配置、部分全局服务和跨解释器协调对象。它的状态天然影响整个进程,因此一旦可变对象留在这一层,就需要全局锁、原子操作、只读化或 immortal object 之类策略。
PyInterpreterState 是解释器级容器。它保存一个解释器运行 Python code 所需的状态,包括模块管理、导入状态、builtins、GC、warnings、atexit、配置副本、线程链表和可选的 interpreter-local GIL。PEP 684 的大量工作可以理解成把过去依赖进程级 GIL 保护的可变状态下移到这一层。
PyThreadState 是线程执行状态容器。它把当前 OS 线程和某个解释器连接起来,保存当前异常、执行中的 frame 相关状态、递归计数、tracing/profiling 状态等。一个解释器可以有多个 thread state;多个解释器也可以分别有各自 thread state。线程切换解释器时,关键动作是切换当前 attached thread state。
这张图给出阅读 CPython 并发代码的稳定骨架。看到一个字段时,先判断它所属层级。如果它在 _PyRuntimeState,并且可变,就会影响所有解释器;如果它在 PyInterpreterState,隔离目标通常是每个解释器一份;如果它在 PyThreadState,它描述的是某个线程当前执行状态。
这一分层也解释了 per-interpreter GIL 的必要条件。GIL 保护的是运行时可变状态。如果大量可变状态仍集中在 _PyRuntimeState 或 C 全局变量,给每个解释器一把 GIL会让多个解释器同时访问同一份状态。PEP 684 的路径先整理全局状态,再把大部分可变状态移动到解释器层,最后让 GIL 跟随解释器。
阅读源码时可以从三个入口建立方向:Include/cpython/pystate.h 和内部头文件描述 PyInterpreterState、PyThreadState 的结构形状;Python/pystate.c 承载创建、清理、切换和遍历状态的逻辑;Python/ceval_gil.c、Python/ceval.c 和相关内部文件展示 GIL、eval loop 和 thread state 如何协作。版本之间文件细节会变化,但“runtime / interpreter / thread”三层判断保持稳定。
47.6 interpreter state
PyInterpreterState 是 subinterpreter 的核心对象。它属于 CPython C 层,用来表示“一个解释器”的状态结构;Python 层通过 concurrent.interpreters.Interpreter 这类对象间接接触它。Python 3.14 C API 文档把它描述为 cooperating threads 共享的状态;线程属于同一个解释器时共享模块管理和若干内部项,线程属于不同解释器时初始状态分离,只共享进程级资源。
在贯穿例子中,每个插件解释器的 PyInterpreterState 至少需要保存这些事实:这个解释器加载了哪些模块,它的 builtins 是哪一份,它的 sys.modules 指向哪张表,它有哪些 thread state,它的 import state 如何处理扩展模块,它的 GC 如何追踪本解释器里的 container object,它的 runtime config 是否允许 fork、exec、daemon thread、single-phase extension,以及它是否使用自己的 GIL。
可以把 PyInterpreterState 看成一次执行隔离的“账本”。插件 A 初始化后,账本记录 A 的模块、类、函数、全局变量、异常状态入口、清理钩子和线程列表。插件 B 有自己的账本。主解释器调度时,通过 interpreter object 或底层 thread state 切换到对应账本执行代码。
# Python-level observation in 3.14: each interpreter has its own __main__ module.
from concurrent import interpreters
interp = interpreters.create()
interp.prepare_main(plugin_id="alpha")
interp.exec('plugin_result = plugin_id.upper()')
这段代码观察到的是 Python 层接口,背后的状态变化发生在目标解释器的 __main__ module namespace。prepare_main() 绑定的名字进入目标解释器;call() 切换到目标解释器执行 callable。参数、callable 和返回值的传递遵循该 API 的 share/copy/serialize 规则,因此这个例子适合说明状态归属;任意 Python 对象跨解释器原样使用仍需符合对应 API 的传递规则。
interpreter state 还包含生命周期责任。创建解释器要初始化模块表、builtins、导入状态、thread state 和配置;关闭解释器要清理线程状态、模块对象、GC 对象和退出钩子。关闭顺序错误会留下对象引用、native 状态或后台线程。对于插件系统,worker 生命周期设计要明确:解释器是按任务创建,还是按插件长驻;缓存是解释器内缓存,还是通过消息传递更新;异常发生后解释器是否销毁重建。
在 C API 中,PyInterpreterState_Get() 返回当前解释器,PyInterpreterState_GetID() 提供解释器 ID,PyInterpreterState_GetDict() 给扩展模块一个解释器级字典位置。扩展模块仍应优先使用 PyModule_GetState() 保存模块状态,因为 module state 的生命周期更贴近模块对象,解释器 dict 更适合少量 interpreter-specific data。
一个可复用的判断顺序是:先定位当前代码在哪个 interpreter state 中执行,再查看模块和对象是否由同一 interpreter state 创建,然后查看 thread state 是否附着到正确解释器,最后查看 C extension 是否把可变状态放在解释器级或模块级位置。这个顺序能把“名字找不到”“模块缓存串扰”“回调进入错误解释器”“扩展模块跨解释器崩溃”归到不同层级。
47.7 object isolation
object isolation 解决的是对象所有权和传递规则。普通 Python 对象由某个解释器创建,并由该解释器的 runtime state、GIL、GC 和 allocator 规则管理。对象跨解释器共享需要满足安全传递规则;常见策略是复制、序列化、使用 runtime 支持的 cross-interpreter data,或在少数受控场景中共享不可变/immortal 对象。
先看引用计数。CPython 对象头里有引用计数,引用计数更新通常依赖当前解释器执行约束。两个解释器各自持有 GIL 时,它们的 GIL 相互独立,因此同一个对象如果被两个解释器直接改引用计数,就失去单一锁保护。PEP 683 的 immortal object 让部分不可变内置对象不再依赖普通引用计数变化,从而为跨解释器安全共享少量对象提供条件。这个能力属于 CPython 实现策略,只覆盖明确纳入该策略的对象。
再看可变容器。list、dict、set、用户实例和大多数自定义对象都携带内部可变状态。即使业务层面“只读使用”,运行时仍可能更新引用计数、缓存、惰性字段、descriptor 绑定或类型相关缓存。跨解释器直接共享这些对象,会把同步责任推给使用者和扩展模块,调试难度接近 free-threading 下的数据竞争。
更稳定的做法是把对象传递设计成消息传递。消息传递要求发送方把对象转换为受支持的数据形状,接收方在自己的解释器中重建对象图。这个模型牺牲了原地共享,换来清晰的所有权边界。
from concurrent import interpreters
queue = interpreters.create_queue()
worker = interpreters.create()
queue.put({"value": 21})
worker.prepare_main(queue=queue)
worker.exec("""
item = queue.get()
result = item["value"] * 2
queue.put({"result": result})
""")
print(queue.get())
这个示例的重点是 queue 作为 cross-interpreter communication object。业务数据通过 queue 传递,目标解释器拿到的是该 API 支持的传递结果。调用方应把这种结果理解为“受规则管理的传递”,而非任意对象引用共享。对于大型对象图,可以考虑 bytes、memoryview、文件、共享内存或应用层协议,但每种方式都要重新确认生命周期、同步和释放责任。
object isolation 也影响异常传播。一个解释器中抛出的异常对象可能包含 traceback、frame、locals 和对象引用链。把完整异常对象直接移动到另一个解释器会携带大量归属复杂的对象。高层 API 通常会保留异常摘要、序列化可传递部分,或用包装异常表达远端执行失败。工程上应记录原始错误、解释器 ID、任务 ID 和输入边界,而不依赖跨解释器保留完整对象图。
最后,把 object isolation 转成检查清单:输入是否可复制或可序列化;输出是否可复制或可序列化;中间对象是否只在 worker 解释器内使用;共享资源是否由专门 channel、queue、file descriptor 或 shared memory 管理;异常和取消是否能通过消息形式返回;扩展模块是否会在对象内部保存跨解释器全局指针。这个清单能决定一个任务是进入 subinterpreter,还是继续使用线程、进程或单解释器队列。
最小自检任务
阅读下面的代码形状,并判断它是否适合放入 InterpreterPoolExecutor。要求说明:任务边界、对象归属、并行收益来源、主要失败点和更稳定的改写方向。
# Python 3.14 shape, simplified for reasoning.
from concurrent.futures import InterpreterPoolExecutor
GLOBAL_CACHE = {"profile": load_large_profile()}
def transform(record: dict[str, str]) -> dict[str, str]:
profile = GLOBAL_CACHE["profile"]
return apply_profile(record, profile)
records = load_records()
with InterpreterPoolExecutor(max_workers=4) as pool:
output = list(pool.map(transform, records))
答案要点
这段代码的任务边界是 record 输入和 transformed dict 输出。InterpreterPoolExecutor 会把 callable、参数和返回值传到 worker 解释器,通常涉及序列化或受支持的数据传递规则,因此 record 和返回 dict 必须满足传递要求。并行收益来自多个 worker 解释器各自拥有 GIL,并由不同线程在不同 CPU core 上执行 apply_profile() 的 Python code。
主要风险集中在 GLOBAL_CACHE。这个名字在主解释器中绑定,worker 解释器执行 transform() 时需要在自己的解释器上下文中拥有可用的函数和模块状态。load_large_profile() 得到的大对象如果尝试作为同一对象引用共享到多个解释器,会破坏对象归属和同步边界;如果每个 worker 各自初始化一份 profile,则内存峰值会上升,但隔离清楚。
更稳定的改写方向是把 profile 初始化放进 worker interpreter 的 initializer 或首次调用缓存中,让每个解释器拥有自己的 profile 对象;输入输出保持 bytes、str、int、tuple、dict 这类可传递数据形状,或显式使用 queue / 序列化协议。若 profile 太大且必须共享,应转向文件映射、共享内存、只读外部服务或进程级资源方案,并为同步和释放设计明确规则。
结论是:这个任务的计算形状可能适合 subinterpreter,但原始写法需要重设缓存归属。判断顺序是先看 callable 和参数如何传递,再看模块级全局状态属于哪个解释器,然后看每个 worker 是否能独立初始化依赖,最后估算计算时间是否覆盖序列化和多份缓存的成本。
本章知识点总结
- subinterpreter:subinterpreter 是同一进程内独立的 CPython 解释器状态,适合表达进程内运行时隔离。
- 主解释器:主解释器是 runtime 初始化时创建的第一个解释器,承担信号处理和初始化、终结阶段的特殊责任。
- GIL 边界:per-interpreter GIL 把锁的保护范围收缩到单个解释器,使不同解释器中的 Python code 具备并行执行潜力。
- 线程关系:线程提供 OS 调度实体,解释器提供 runtime state,
PyThreadState把当前线程连接到某个解释器。 - 模块隔离:每个解释器拥有自己的
sys.modules、builtins 和导入状态,同一模块在不同解释器中通常对应不同 module object。 - 对象归属:普通 Python 对象属于创建它的解释器,跨解释器直接共享会破坏引用计数、GC 和内部状态同步边界。
- 数据传递:跨解释器通信应优先使用复制、序列化、queue 或 runtime 支持的 cross-interpreter data。
- 扩展模块:C extension 需要通过多阶段初始化、per-module state 和兼容声明来支持多解释器隔离。
- 并行收益:subinterpreter 的收益来自独立 CPU 任务、清晰输入输出和足够大的任务粒度。
- 并行成本:序列化、复制、多份缓存、解释器管理和扩展兼容性会抵消小任务的并行收益。
- 状态分层:
_PyRuntimeState保存进程级状态,PyInterpreterState保存解释器级状态,PyThreadState保存线程执行状态。 - 工程判断:选择 subinterpreter 时,应依次检查状态归属、对象传递、扩展兼容、任务粒度、异常传播和生命周期清理。