欢迎光临
我们一直在努力

Python Subinterpreter 到底解决了什么?线程、进程与子解释器的并行架构选型指南

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 一张表看懂

把项目最关心的几个指标放在一起:

维度ThreadSubinterpreterProcess
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任务调度

赞(0)
未经允许不得转载:171主机测评 » Python Subinterpreter 到底解决了什么?线程、进程与子解释器的并行架构选型指南
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址