Python Subinterpreter 到底解决了什么?线程、进程与子解释器的并行架构选型指南
在 Python 并发编程里,我们长期面对一个看似简单、实际上很棘手的问题:
我既想要线程一样低的启动成本,又想要进程一样的多核并行,同时还希望不同任务之间互不干扰,可以吗?
过去,答案通常只有两个:
I/O 密集 → Thread
CPU 密集 → Process
但今天,这个二分法已经不完整了。
CPython 很早就拥有 Subinterpreter(子解释器)的底层能力,而真正让它进入普通 Python 开发者视野的是近几年的两个重要变化:Python 3.12 引入了 per-interpreter GIL,允许隔离的解释器拥有自己的 GIL;Python 3.14 又正式加入 concurrent.interpreters 和 InterpreterPoolExecutor,让开发者不必直接操作复杂的 C API,也能在一个进程里使用多个解释器。(Python Enhancement Proposals (PEPs))
于是,Python 并发架构多了第三种选择:
Thread
Process
Subinterpreter
问题也随之而来:
Subinterpreter 到底解决了什么?它是不是“轻量级 Process”?能不能拿它执行不同用户提交的 Python Task?
这篇文章就把这些问题一次讲透。
一、先建立正确的 Python Runtime 心智模型
理解 Subinterpreter,首先要区分四个东西:
Machine
│
└── Process
│
└── Python Runtime
│
├── Interpreter A
│ ├── Thread 1
│ └── Thread 2
│
└── Interpreter B
└── Thread 3
我们平时启动:
python app.py
一般会得到:
1 Process
+
1 Main Interpreter
+
1 Main Thread
如果使用:
threading.Thread(...)
得到的是:
1 Process
1 Interpreter
N Threads
而 Subinterpreter 是:
1 Process
N Interpreters
N Threads(通常每个 worker 对应一个)
进程则是:
N Processes
每个 Process 各自拥有 Python Runtime / Interpreter
这个区别至关重要。
CPython 官方把 interpreter 描述成 Python Runtime 的一个执行上下文,其中拥有自己的解释器状态,例如 import 状态、sys.modules、builtins 等。不同 Subinterpreter 之间大部分这些状态相互隔离。(Python documentation)
于是三种并发模型可以粗略理解成:
Thread
= 共享解释器
Subinterpreter
= 共享进程,不共享解释器状态
Process
= 连进程都不共享
二、Thread 最大的问题不是“线程慢”,而是共享得太多
先看最熟悉的线程:
from concurrent.futures import ThreadPoolExecutor
def work(x):
return x * x
with ThreadPoolExecutor(max_workers=4) as pool:
results = list(pool.map(work, range(10)))
线程的优势非常明显:
创建快
通信快
共享对象容易
内存开销低
因为所有线程生活在:
同一个 Process
+
同一个 Interpreter
因此:
cache = {}
config = {}
model = ...
所有线程理论上都可以直接访问。
这既是优势,也是危险。
假设:
counter = 0
多个线程:
counter += 1
你马上进入:
race condition
locking
deadlock
shared mutable state
的世界。
而在传统 GIL 构建的 CPython 中,同一解释器通常只能有一个线程持有 GIL 执行 Python 对象相关操作,因此 CPU 密集型纯 Python 代码通常无法靠普通线程真正跑满多个核心。阻塞 I/O、释放 GIL 的 C 扩展以及 free-threaded 构建则属于不同情况。(Python documentation)
所以:
Thread
最舒服的场景仍然是:
HTTP 请求
数据库查询
文件 I/O
Socket
等待型任务
三、Process 为什么一直是 CPU 并行的标准答案?
再看进程:
from concurrent.futures import ProcessPoolExecutor
def cpu_task(n):
total = 0
for i in range(n):
total += i * i
return total
if __name__ == "__main__":
with ProcessPoolExecutor(max_workers=4) as pool:
futures = [
pool.submit(cpu_task, 10_000_000)
for _ in range(4)
]
print([f.result() for f in futures])
这里每一个 worker 都是独立进程:
Main Process
│
├── Worker Process 1
├── Worker Process 2
├── Worker Process 3
└── Worker Process 4
每个进程有自己的:
address space
Python interpreter
GIL
module state
heap
GC
所以天然可以:
CPU0 → Process 1
CPU1 → Process 2
CPU2 → Process 3
CPU3 → Process 4
实现真正的多核并行。
ProcessPoolExecutor 正是通过独立进程绕过单解释器 GIL,同时要求提交的 callable、参数和返回值满足可序列化要求。(Python documentation)
但 Process 也要付钱。
代价包括:
进程创建
独立地址空间
IPC
pickle
数据复制
模块重新加载
操作系统调度
更高内存占用
如果 task 本身只有:
1 ms
却花大量时间完成:
serialize
IPC
deserialize
那 Process 的收益可能被通信成本吃掉。
四、Subinterpreter 正好站在 Thread 与 Process 中间
现在终于轮到主角。
Python 3.14 可以直接使用:
from concurrent import interpreters
interp = interpreters.create()
interp.exec("""
x = 10
y = 20
print(x + y)
""")
interp.close()
注意:
interp.exec(...)
本身并不会自动创建并发。
解释器只是一个独立执行上下文。
真正做并发,需要把不同解释器放到不同线程运行。
因此实际工程中,更方便的是 Python 3.14 新增的:
InterpreterPoolExecutor
例如:
from concurrent.futures import InterpreterPoolExecutor
def cpu_task(n):
total = 0
for i in range(n):
total += i * i
return total
with InterpreterPoolExecutor(max_workers=4) as pool:
futures = [
pool.submit(cpu_task, 10_000_000)
for _ in range(4)
]
print([f.result() for f in futures])
表面上和:
ThreadPoolExecutor
ProcessPoolExecutor
几乎一样。
但内部模型完全不同:
One Process
│
├── Thread 1 → Interpreter 1 → GIL 1
├── Thread 2 → Interpreter 2 → GIL 2
├── Thread 3 → Interpreter 3 → GIL 3
└── Thread 4 → Interpreter 4 → GIL 4
InterpreterPoolExecutor 本身是 ThreadPoolExecutor 的子类,但每个 worker thread 都拥有自己的 interpreter。因为每个 interpreter 有自己的 GIL,所以不同 worker 可以同时执行 Python 代码,实现真正的多核并行。(Python documentation)
这正是 Subinterpreter 最重要的意义。
五、Thread、Process、Subinterpreter 一张表看懂
把项目最关心的几个指标放在一起:
| Python 状态隔离 | 很弱 | 较强 | 很强 |
| 地址空间隔离 | 无 | 无 | 有 |
| 普通对象直接共享 | 容易 | 通常不行 | 通常不行 |
| 内存开销 | 最低 | 中等 | 通常最高 |
| 创建成本 | 最低 | 中等 | 通常最高 |
| CPU 多核并行 | 传统 GIL 下有限 | 可以 | 可以 |
| sys.modules | 共享 | 独立 | 独立 |
| module globals | 共享 | 通常独立 | 独立 |
| 数据通信成本 | 最低 | 中等 | 较高 |
| C 扩展兼容性 | 最成熟 | 需要验证 | 通常最好 |
| worker 崩溃隔离 | 很弱 | 很弱 | 较强 |
| 安全沙箱 | 否 | 否 | 单独进程仍需 OS 沙箱 |
| 适合不可信代码 | 否 | 否 | 配合容器/权限隔离后可以 |
这里尤其要注意三个容易产生误解的地方。
六、第一个误区:Subinterpreter 不是共享一切的“多 GIL Thread”
每个 interpreter 都拥有独立 runtime state。
例如 Interpreter A:
import myapp
myapp.counter = 100
Interpreter B 再:
import myapp
看到的是自己解释器中的模块实例。
官方文档明确指出,每个 worker interpreter 有自己的模块对象,包括:
sys
builtins
__main__
一个解释器里执行:
sys.stdout = ...
不会自动修改另一个解释器。一个解释器 import 了某个模块,也不意味着另一个解释器已经 import。(Python documentation)
所以:
Thread:
共享默认开启
Subinterpreter:
共享需要显式设计
我很喜欢用一句话概括它:
Thread 是 shared-by-default,Subinterpreter 更接近 isolation-by-default。
这让很多并发程序反而更容易推理。
七、第二个误区:用了 Subinterpreter 就不用序列化了
并不是。
例如:
with InterpreterPoolExecutor() as pool:
future = pool.submit(
task,
some_large_object,
)
InterpreterPoolExecutor 会使用 pickle 序列化 callable 和参数,再交给 worker interpreter;返回值同样需要序列化回来。(Python documentation)
所以它不是:
Thread 那样直接传 Python object pointer
而更接近:
Main Interpreter
│
pickle
↓
Worker Interpreter
│
compute
│
pickle
↓
Main Interpreter
这意味着如果:
task = 100 μs
payload = 100 MB
那么 Subinterpreter 很可能不是你的救星。
真正适合的任务更像:
输入很小
计算很多
输出较小
例如:
def score_user(user_id: int) –> float:
...
而不是:
def process(huge_dataframe):
...
八、解释器之间怎么通信?
Python 3.14 的 concurrent.interpreters 提供了跨解释器 Queue。
例如概念上:
from concurrent import interpreters
queue = interpreters.create_queue()
然后不同解释器可以通过消息传递交换数据。
这体现了 Subinterpreter 比较推荐的并发哲学:
Don't share mutable Python objects.
Pass messages instead.
大多数对象跨解释器时仍需要复制或 pickle;部分不可变内置对象可以直接共享或高效复制,标准库还提供了少数特别设计的跨解释器共享机制。(Python documentation)
因此系统最好设计成:
Scheduler
│
small message
│
┌──────────┼──────────┐
▼ ▼ ▼
Interp A Interp B Interp C
│ │ │
Task Task Task
│ │ │
└──────────┼──────────┘
▼
small result
而不是:
所有 interpreter
共同修改一个巨大 Python dict
否则你又把自己带回了共享状态地狱。
九、真正的大坑:Extension Compatibility
这是决定 Subinterpreter 能不能上线的核心问题之一。
纯 Python module 通常比较容易处理。
真正麻烦的是:
C Extension
C++
Cython
native runtime
third-party library
例如某个扩展内部可能有:
static PyObject *global_cache;
如果它默认认为:
一个 Process
=
一个 Python Interpreter
那么现在突然出现:
一个 Process
=
多个 Interpreter
它的假设可能直接失效。
CPython 推荐 C 扩展采用 multi-phase initialization,并把 interpreter-specific state 放进对应的 module instance,而不是随意依赖进程级 static/global 状态。扩展还可以明确声明自己不支持 multiple interpreters。(Python documentation)
所以项目迁移前,一定要做:
Dependency Audit
特别检查:
NumPy
Pandas
PyTorch
TensorFlow
数据库驱动
压缩库
加密库
自研 C Extension
Cython Extension
ctypes binding
不要因为:
import some_package
在主解释器能运行,就默认:
InterpreterPoolExecutor
里一定没问题。
Subinterpreter 目前最需要敬畏的地方之一,就是生态兼容性仍然不像普通线程和独立进程那么透明。Python 官方也明确提醒,并非所有 PyPI 包都已经支持 multiple interpreters。(Python documentation)
十、内存真的会比 Process 小很多吗?
通常:
Thread < Subinterpreter < Process
但不要把它理解成严格公式。
Thread 几乎共享整个 interpreter:
modules
heap
objects
code
所以额外内存最少。
Subinterpreter 虽然仍在同一个 Process 中,却拥有自己的:
sys.modules
module objects
builtins
GC state
interpreter state
模块通常需要分别初始化。
假设:
import heavy_package
一个解释器额外消耗:
200 MB
8 个 interpreter 不代表只付一次这 200 MB。
另一方面,Process 的行为还取决于:
spawn
fork
forkserver
OS
copy-on-write
因此不要只凭“Subinterpreter 在一个 Process 里”就认定内存一定能降低一个数量级。
正确做法仍然是 Benchmark:
RSS
USS
PSS
startup latency
task latency
throughput
而不是凭架构名字猜。
另外,从 Python 3.14 开始,ProcessPoolExecutor 默认 multiprocessing start method 已经改为不再默认使用 fork,如果确实依赖 fork,需要显式选择。(Python documentation)
这也使“Process 启动一定非常便宜”的一些旧经验更需要重新测量。
十一、Startup 成本怎么比较?
工程上通常可以预期:
Thread
↓
Subinterpreter
↓
Process
从轻到重。
但这里千万别背固定数字。
Subinterpreter 的启动成本包括:
创建 interpreter state
初始化 runtime state
import dependency
initializer
如果每个 interpreter 都需要:
import pandas
import numpy
import my_big_framework
启动成本就可能很明显。
因此不要:
for task in tasks:
interp = create_interpreter()
run(task)
destroy_interpreter()
更合理的是:
长期 Interpreter Pool
│
├── task
├── task
├── task
└── task
也就是:
with InterpreterPoolExecutor(
max_workers=8,
initializer=init_worker,
) as pool:
...
一次初始化,重复使用。
十二、项目实战:让不同用户提交 Python Task,应该选什么?
现在来看最关键的问题。
需求:
在一个进程内,并行执行不同用户提交的 Python task。
看起来 Subinterpreter 简直完美:
一个 Process
+
每个用户一个 Interpreter
+
每个 Interpreter 一个 GIL
+
多核并行
但在决定架构前,我会先问一句:
这些用户代码可信吗?
因为答案不同,架构会完全不同。
十三、场景 A:公司内部、可信代码
假设所谓“用户提交代码”其实是:
内部工程师提交任务
审核过的插件
受控算法
平台自己的函数
固定 dependency whitelist
这时 Subinterpreter 非常值得评估。
架构可以是:
API
│
▼
Scheduler
│
┌─────────┼─────────┐
▼ ▼ ▼
Interpreter Interpreter Interpreter
Worker 1 Worker 2 Worker 3
│ │ │
Task A Task B Task C
代码:
from concurrent.futures import (
InterpreterPoolExecutor,
as_completed,
)
def execute_task(task):
# task 应尽量是小型、可 pickle 的输入
return task.run()
def run_tasks(tasks):
results = []
with InterpreterPoolExecutor(
max_workers=8,
) as executor:
futures = [
executor.submit(execute_task, task)
for task in tasks
]
for future in as_completed(futures):
try:
results.append(future.result())
except Exception as exc:
results.append({
"ok": False,
"error": repr(exc),
})
return results
这种场景下它可能同时获得:
比线程更强的状态隔离
+
真正 CPU 多核
+
比多进程更轻的整体运行模型
非常有吸引力。
十四、场景 B:真正不可信的用户代码
例如用户可以提交:
import os
甚至:
import ctypes
或者任意第三方扩展。
那么答案必须改变:
不要把 Subinterpreter 当安全沙箱。
Python 官方文档对此非常明确:同一进程中的多个 interpreter 从技术上不可能实现严格内存隔离,底层操作甚至可能影响彼此;因此,不应在安全敏感、彼此不应访问数据的场景下使用 Subinterpreter 作为安全边界。(Python documentation)
例如两个 Subinterpreter 最终仍然属于:
Same PID
Same address space
Same OS process
如果 native extension 出现:
segmentation fault
你可能得到的不是:
Task B failed
而是:
整个 Process 没了
这就是 Subinterpreter 和 Process 最大的本质差异之一。
十五、不可信 Task 的正确架构是什么?
我会直接放弃:
“必须在一个 Process 内安全执行任意用户 Python”
这个约束。
因为它和真正的安全隔离目标冲突。
更合理的架构是:
Scheduler
│
┌──────────┼──────────┐
▼ ▼ ▼
Sandbox 1 Sandbox 2 Sandbox 3
│ │ │
Python Python Python
Process Process Process
甚至进一步:
Container
+
separate UID
+
filesystem isolation
+
network policy
+
CPU quota
+
memory quota
+
timeout
+
syscall restriction
也就是说:
Process isolation
只是第一层。
真正的不可信代码执行通常还需要:
OS sandbox
container
namespace
cgroup
seccomp / platform equivalent
filesystem permission
network restriction
resource quota
Process 本身也不等于安全沙箱。
但它至少提供了 Subinterpreter 不具备的地址空间和 OS 级故障边界。
Python 3.14 的 ProcessPoolExecutor 甚至增加了 terminate_workers() 与 kill_workers(),为强制终止 worker 提供了标准接口。(Python documentation)
对于可能:
while True:
pass
的用户代码,这种强终止能力非常重要。
十六、Timeout 也是 Subinterpreter 方案容易忽略的问题
很多人会写:
future.result(timeout=5)
然后觉得:
5 秒以后 task 会被杀死
这是错误理解。
timeout 通常只意味着:
我不继续等待结果
并不等于:
正在执行的代码已经停止
Future.cancel() 对已经 running 的任务同样不是一个通用的“杀死代码”按钮。
于是对于:
while True:
pass
这种恶意或失控任务:
Thread
难以安全强杀。
Subinterpreter worker thread
同样没有独立 OS process 那种清晰的 kill boundary。
而独立 Process:
terminate
kill
restart
就容易得多。
所以只要产品需求中出现:
用户脚本
硬超时
恶意代码
无限循环
OOM
native crash
我的倾向都会迅速转向:
Process / Container
十七、如何系统评估三种方案?
我通常会做一个 Decision Matrix。
任务主要是 I/O?
优先:
Thread
因为:
最简单
最低开销
对象共享方便
是 CPU 密集型纯 Python,代码可信?
开始比较:
Subinterpreter
vs
Process
如果:
task 输入输出较小
依赖兼容
希望低一些运行开销
希望一个 Process 管理
那么:
InterpreterPoolExecutor
非常值得 Benchmark。
大量使用 C Extension?
先测试:
Subinterpreter compatibility
无法确认时:
Process
仍然是更保守的选择。
Task 之间需要大量共享 mutable object?
如果主要是 I/O:
Thread
可能更自然。
Subinterpreter 强行共享复杂可变状态通常违背它的设计优势。
Task 不可信?
直接:
Process
+
OS sandbox
不要把安全责任交给 Subinterpreter。
十八、我的生产选型表
可以把最终策略压缩成这样:
┌─ I/O bound
│
▼
Thread
Task ── trusted? ── YES ─┤
│
└─ CPU bound
│
extensions compatible?
│
YES ────┴──── NO
│ │
▼ ▼
Subinterpreter Process
Task ── trusted? ── NO
│
▼
Isolated Process
│
▼
OS Sandbox
这张图基本能覆盖大多数 Python 后端、计算平台和任务执行系统。
十九、Subinterpreter 真正解决的是什么?
看到这里,就可以回答标题的问题了。
Subinterpreter 并不是为了:
取代 Thread
也不是为了:
取代 Process
它解决的是过去 Python 架构里一个长期存在的空缺:
我希望在同一个 OS Process 里维护多个相对独立的 Python Runtime Context,同时让 CPU 密集型 Python 代码可以跨核心并行。
换句话说:
Thread
偏向共享
Process
偏向隔离
Subinterpreter
试图在二者之间找到新的平衡
它最诱人的地方,不单纯是性能。
而是:
Isolation
+
Parallelism
+
In-process execution
三者第一次以标准库形式真正组合到了一起。
二十、但别忽略它真正的价格
Subinterpreter 给你的东西是:
独立 sys.modules
独立 runtime state
独立 GIL
多核并行
一个 Process 内运行
而你需要付出的代价则是:
对象不能再随便共享
通信需要重新设计
pickle 仍然存在
模块需要分别加载
内存并非零成本
C Extension 需要验证
native crash 仍可能拖垮全进程
不能作为安全 sandbox
所以生产环境里最危险的问题不是:
“Subinterpreter 快不快?”
而是:
“我的应用边界,究竟是并发边界、故障边界,还是安全边界?”
如果只是并发边界:
Subinterpreter
可能非常精彩。
如果需要的是故障边界:
Process
通常更合适。
如果需要的是安全边界:
Process + OS Sandbox
才是应该讨论的起点。
二十一、最后给一个非常实用的选择口诀
日常 Python 实战中,可以先记住:
I/O 多:
Thread
CPU 多、代码可信、依赖兼容:
Subinterpreter 值得测
CPU 多、生态复杂:
Process 更稳
代码不可信:
Process / Container + Sandbox
不要因为 Subinterpreter 更新、更酷,就强行使用它。
也不要因为 Process 已经用了很多年,就忽视新的可能性。
优秀的 Python 工程设计,从来不是寻找“最先进”的并发模型,而是找到最匹配故障模型、数据模型和安全模型的执行边界。
如果未来你正在建设:
插件系统
计算平台
规则引擎
Python Function Runtime
内部任务平台
Notebook Backend
多租户计算服务
那么 Subinterpreter 很值得认真 Benchmark。
但如果你的需求是:
“我要在一个进程里安全执行互不信任用户提交的任意 Python。”
那么最专业的答案,反而可能是:
不要坚持一个进程。
因为有些边界,可以通过更聪明的 Python API 优化;而有些边界,必须由操作系统替你守住。
参考资料与延伸阅读
截至 2026 年 9 月,Python 3.14 标准库已经提供 concurrent.interpreters 和 InterpreterPoolExecutor。官方资料建议重点阅读:
Python concurrent.interpreters 文档:介绍多解释器隔离、通信以及对象共享规则。(Python documentation)
Python concurrent.futures 文档:详细介绍 ThreadPoolExecutor、InterpreterPoolExecutor 与 ProcessPoolExecutor。(Python documentation)
PEP 684:解释 Per-Interpreter GIL 的设计与多核并行目标。(Python Enhancement Proposals (PEPs))
PEP 734:介绍标准库 Multiple Interpreters API 与解释器间通信设计。(Python Enhancement Proposals (PEPs))
Python C API Multiple Interpreters 文档:如果项目使用 C/C++ 扩展,这部分尤其值得阅读。(Python documentation)
留给读者的思考
如果你的服务中有 100 个可信 Python 插件,每个插件每天执行数百万次 CPU 计算,你会选择:
100 个 Process
还是:
一个 Process + Interpreter Pool
再进一步,如果这些插件突然变成:
外部客户上传的任意 Python 脚本
你的答案是否应该立刻改变?
这正是 Subinterpreter 最值得讨论的地方——它不只是一个新的 Python API,也迫使我们重新思考:
并发
隔离
故障
安全
究竟是不是同一个问题。
答案往往是:不是。
关键词: Python编程、Python教程、Python实战、Python最佳实践、Subinterpreter、InterpreterPoolExecutor、ThreadPoolExecutor、ProcessPoolExecutor、Python并发、GIL、多核并行、Python任务调度




