← 返回题目列表

Python 的子解释器是什么?它和多线程、多进程有什么区别?

困难 第 27 / 27 题 更新于 2026/08/01
子解释器PEP 734per-interpreter GIL并发

简化版

「子解释器(subinterpreter)」是在同一个进程里创建多个相互独立的 Python 解释器实例——每个有自己的模块表、内置类型、sys.modules、以及自己的 GIL**。关键突破是 Python 3.12 的 PEP 684「per-interpreter GIL」:在此之前所有子解释器共用一把 GIL(所以并不能真正并行),3.12 起每个子解释器拥有独立的 GIL**,因此多个子解释器可以在多核上真正并行执行 Python 字节码Python 3.13 通过 PEP 734 提供了官方的 interpreters 模块(3.13 里放在 test.support.interpreters,3.14 起正式为 concurrent.interpreters),并配套了 InterpreterPoolExecutor它的定位是「比线程隔离、比进程轻量」的中间形态:解释器之间对象不共享(每个有自己的堆和引用计数),数据要通过 Queue/Channel 传递(会做序列化或走特殊的共享内存通道);但它们共享同一个进程的内存空间和文件描述符,所以创建开销比进程小得多(微秒到毫秒级 vs 进程的几十到几百毫秒),也不需要走操作系统的 IPC。和自由线程(no-GIL)的区别要分清子解释器 = 多个解释器、各有 GIL、对象不共享、胜在隔离;自由线程 = 一个解释器、没有 GIL、对象直接共享、胜在效率——它们是两条独立的技术路线。最大的现实约束是 C 扩展:大量扩展使用了进程级的静态全局状态,在多个子解释器里会互相污染甚至崩溃,必须适配多阶段初始化(PEP 489/630)才能安全使用。核心记忆:一个进程、多个解释器、各自一把 GIL;对象不共享、靠队列通信;定位是「轻量进程」而不是「无 GIL 线程」

详细版

四种并发模型的定位

维度多线程(有 GIL)子解释器多进程自由线程(no-GIL)
CPU 并行(3.12+ 各有 GIL)
创建开销微秒毫秒级几十~几百毫秒微秒
内存开销极低(每个解释器一套模块)(整个进程)极低
对象共享✅ 直接不共享✅ 直接
数据传递直接引用Queue/Channel(拷贝或共享内存)pickle + IPC直接引用
隔离性❌ 全局状态互串各有 sys.modules✅✅ 完全隔离
崩溃影响全挂全挂(同一进程)只挂一个全挂
C 扩展全部可用⚠️ 需适配全部可用⚠️ 需适配
# ① Python 3.14+:concurrent.interpreters(3.13 是 test.support.interpreters)
from concurrent import interpreters

interp = interpreters.create()            # ★创建一个全新的解释器实例★
interp.exec("""
import math
print("子解释器里:", math.sqrt(16))
""")                                       # ★在子解释器里执行代码★
interp.close()

# ② 传值:只能传"可共享"的简单对象
interp = interpreters.create()
interp.prepare_main(x=10, name="task")     # ★把简单值注入子解释器的 __main__★
interp.exec("print(x, name)")

# ③ ★队列通信(PEP 734 的核心 API)★
queue = interpreters.create_queue()
interp = interpreters.create()
interp.exec(f"""
from concurrent import interpreters
q = interpreters.Queue({queue.id})
q.put("来自子解释器的消息")
""")
print(queue.get())

# ④ ★InterpreterPoolExecutor(3.14+):像线程池一样用★
from concurrent.futures import InterpreterPoolExecutor
def cpu_work(n):
    return sum(i * i for i in range(n))
with InterpreterPoolExecutor(max_workers=4) as ex:
    results = list(ex.map(cpu_work, [10**7] * 4))   # ★真正并行(各有 GIL)★

# ⑤ ★隔离性演示:全局状态互不影响★
import sys
interp = interpreters.create()
interp.exec("import sys; sys.modules['mymod'] = 'child'")
print('mymod' in sys.modules)              # False ← ★主解释器完全不受影响★

# ⑥ ★不能直接传对象★
obj = {"a": [1, 2, 3]}
# interp.exec(f"process({obj})")           # ✗ 只能传字符串代码
# 必须通过 queue(会被序列化/共享)或 prepare_main 传简单值

# ⑦ C 扩展的兼容性检查
interp = interpreters.create()
try:
    interp.exec("import numpy")            # ★未适配的扩展会 ImportError★
except interpreters.ExecutionFailed as e:
    print("这个扩展还不支持子解释器:", e)

⚠️ 三个必须分清的概念:① 子解释器 ≠ 自由线程(no-GIL),它们是两条独立的技术路线:子解释器是「一个进程、多个解释器实例、每个有自己的 GIL、对象不共享」——本质是「更轻的进程」;自由线程是「一个解释器、多个线程、没有 GIL、对象直接共享」——本质是「真正的线程」。前者胜在隔离性(一个解释器里的全局状态、猴子补丁、模块级配置不会影响别的),后者胜在共享效率(零拷贝)。两者可以并存。② 每个子解释器要重新 import 所有模块——它有独立的 sys.modules,所以创建开销是毫秒级(比线程的微秒级重得多),而且每个解释器都要占一份模块和类型对象的内存。这决定了它适合「创建少量、长期复用」(配合 InterpreterPoolExecutor),而不是频繁创建销毁。③ 最大的现实障碍是 C 扩展:传统 C 扩展用进程级的静态全局变量保存状态,多个子解释器同时用会互相覆盖甚至崩溃;要安全支持必须改造成 PEP 489 多阶段初始化 + PEP 630 隔离模块状态——目前只有部分主流库完成了适配,这也是子解释器至今仍属「前沿特性」的主要原因。

完整版教学

一、子解释器是什么:一个进程里的多个 Python

正常情况:一个 Python 进程 = 一个解释器实例
  ┌──────────── 进程 ────────────┐
  │  解释器状态                   │
  │   ├ sys.modules(模块表)      │
  │   ├ 内置类型对象               │
  │   ├ 全局变量、单例             │
  │   ├ ★GIL★                     │
  │   └ 线程 1、线程 2…(共享以上) │
  └──────────────────────────────┘

子解释器:同一个进程里开出多套上述状态
  ┌────────────────── 进程 ──────────────────┐
  │ ┌── 解释器 A ──┐  ┌── 解释器 B ──┐        │
  │ │ sys.modules  │  │ sys.modules  │        │
  │ │ 类型对象      │  │ 类型对象      │        │
  │ │ ★GIL A★      │  │ ★GIL B★      │  ←3.12+ │
  │ │ 线程 1        │  │ 线程 2        │        │
  │ └──────────────┘  └──────────────┘        │
  │        共享:进程内存空间、fd、信号处理       │
  └──────────────────────────────────────────┘

  ★ 关键:解释器 A 和 B 的对象★互不可见★
    A 里 import 的模块、定义的类、打的猴子补丁,B 完全不知道
    → 这是"隔离"的来源

三个 PEP 的关系(★面试要能理清★):
  ★PEP 554(2017 提出)★ —— 暴露 interpreters 模块的 Python API
     (长期停留在草案,最终被 PEP 734 取代)
  ★PEP 684(Python 3.12)★ —— ★per-interpreter GIL★
     把原本进程唯一的 GIL 拆成"每个解释器一把"
     → ★这是让子解释器真正能并行的关键一步★
     → 3.12 只提供了 C API,Python 层还用不了
  ★PEP 734(Python 3.13/3.14)★ —— 官方的 Python 层 API
     3.13:test.support.interpreters(实验性)
     3.14:★concurrent.interpreters★(正式)+ InterpreterPoolExecutor

  ★ 时间线记忆:
    3.12 打通底层(各自的 GIL)→ 3.13 实验性 API → 3.14 正式模块

历史:子解释器其实很老
  Py_NewInterpreter() 这个 C API ★1997 年就有了★
  → mod_wsgi 用它给不同的 Web 应用做隔离
  → 但当时★所有子解释器共用一把 GIL★ → 只有隔离、没有并行
  → PEP 684 才让"多解释器 = 多核并行"成为可能

子解释器就是在同一个进程里开出多套解释器状态:每个有独立的 sys.modules、内置类型对象、全局变量,3.12 起还有各自的 GIL。关键在于解释器之间的对象互不可见——A 里 import 的模块、定义的类、打的猴子补丁,B 完全不知道,这正是「隔离」的来源。三个 PEP 的关系要理清:PEP 554(2017)提出 Python 层 API(长期停留草案,最终被取代);PEP 684(3.12)实现了 per-interpreter GIL,把原本进程唯一的 GIL 拆成每个解释器一把——这是让子解释器真正能并行的关键一步,但 3.12 只提供了 C API;PEP 734 才提供官方的 Python 层 API(3.13 实验性、3.14 正式成为 concurrent.interpreters 并配套 InterpreterPoolExecutor)。有意思的是子解释器这个概念其实很老——Py_NewInterpreter() 这个 C API 1997 年就有了(mod_wsgi 用它隔离不同的 Web 应用),但那时所有子解释器共用一把 GIL,只有隔离没有并行。

二、per-interpreter GIL:为什么能并行

3.12 之前:GIL 是★进程级★的
  进程 ──► 唯一的 GIL ──► 所有解释器、所有线程抢它
  → 子解释器只能提供"命名空间隔离",★CPU 并行仍然做不到★

3.12(PEP 684):GIL 变成★解释器级★的
  解释器 A ──► GIL A          解释器 B ──► GIL B
  → A 的线程持有 GIL A 时,B 的线程可以同时持有 GIL B
  → ★两个解释器的字节码真正并行执行在两个核上★

  为了做到这一点,CPython 必须把大量★进程级全局状态★改成★解释器级★:
    - 内置类型对象(int、str、list 的类型对象本身)
    - 小整数缓存、interned 字符串池
    - 内存分配器的 arena
    - 各种缓存和单例
  ★ 这是一个持续多个版本的巨大重构("运行时状态隔离"工作)

  代价:
    ① ★每个解释器都要有自己的一套内置类型和缓存★→ 内存开销
    ② 创建一个解释器要初始化这一整套 → ★毫秒级★(不像线程那样微秒级)
    ③ 部分对象(如不朽对象、某些常量)仍然共享以节省内存

★ 与 no-GIL(PEP 703)的路线对比:
  ┌──────────────┬────────────────────────┬──────────────────────────┐
  │              │ 子解释器(PEP 684/734) │ 自由线程(PEP 703)        │
  ├──────────────┼────────────────────────┼──────────────────────────┤
  │ 解释器数量    │ ★多个★                 │ 一个                      │
  │ GIL          │ ★每个解释器一把★        │ ★没有★                    │
  │ 对象共享      │ ❌ 不共享(要传递)      │ ✅ ★直接共享★             │
  │ 数据传递成本  │ 序列化/共享内存          │ ★零(同一个对象)★        │
  │ 隔离性        │ ✅ ★强★                │ ❌ 全局状态互串            │
  │ 竞态风险      │ 低(不共享就没有竞态)    │ ⚠️ ★完全暴露★             │
  │ 单线程性能    │ 不变                    │ ★慢 5~10%★               │
  │ C 扩展要求    │ 隔离模块状态(PEP 630)  │ 线程安全(Py_MOD_GIL_NOT_USED)│
  │ 成熟度(2026) │ 3.14 正式 API,生态早期  │ 3.14 官方支持,生态早期    │
  └──────────────┴────────────────────────┴──────────────────────────┘

  ★ 一句话区分:
    子解释器 = ★"更轻的进程"★(隔离 + 消息传递)
    自由线程 = ★"真正的线程"★(共享 + 需要加锁)

per-interpreter GIL(PEP 684,Python 3.12)是让子解释器真正有用的关键:3.12 之前 GIL 是进程级的,所有解释器抢同一把锁——子解释器只能提供命名空间隔离,CPU 并行仍然做不到;3.12 起 GIL 变成解释器级的,A 的线程持有 GIL A 时 B 的线程可以同时持有 GIL B,两个解释器的字节码真正并行执行在两个核上。为此 CPython 必须把大量进程级全局状态改成解释器级(内置类型对象、小整数缓存、interned 字符串池、内存分配器 arena),这是一场持续多个版本的巨大重构,代价是每个解释器都要有自己的一套内置类型和缓存(内存开销)以及创建要毫秒级。和自由线程的区别可以用一句话概括:子解释器 = 「更轻的进程」(隔离 + 消息传递),自由线程 = 「真正的线程」(共享 + 需要加锁)

三、API 与数据传递

基本用法(★3.14 的 concurrent.interpreters★):
  from concurrent import interpreters

  interp = interpreters.create()          # 创建
  interp.exec("code")                     # ★执行代码字符串★
  interp.call(func, *args)                # 调用可 pickle 的函数(3.14)
  interp.prepare_main(x=1, s="abc")       # 注入简单值到子解释器的 __main__
  interp.close()                          # 销毁
  interpreters.list_all()                 # 列出所有解释器
  interpreters.get_current()              # 当前解释器

★ 数据传递:不能直接传对象★
  每个解释器有★自己的堆和引用计数★
  → 一个对象的指针在另一个解释器里是无效的(引用计数会错乱)
  → 必须通过受支持的机制传递

  ① ★可共享的类型(免拷贝或低成本)★:
     None、bool、int、float、bytes、str、tuple(元素也要可共享)
     memoryview(★真正的零拷贝共享内存★)
     interpreters.Queue / Channel 本身
  ② ★Queue(PEP 734 的主要通道)★:
     q = interpreters.create_queue()
     q.put(obj)      # 简单对象直传;★复杂对象会被序列化(默认 pickle)★
     q.get()
  ③ ★共享内存★:multiprocessing.shared_memory 或 memoryview
     → 大数组的正确传法(★零拷贝★)
  ④ 不能传:普通的自定义对象、函数闭包、打开的文件对象、锁

★ InterpreterPoolExecutor(3.14+,最实用的入口):
  from concurrent.futures import InterpreterPoolExecutor
  with InterpreterPoolExecutor(max_workers=4,
                               initializer=setup,      # 每个解释器初始化一次
                               ) as ex:
      futs = [ex.submit(cpu_task, n) for n in data]
      for f in futs: print(f.result())

  ★ 和 ThreadPoolExecutor / ProcessPoolExecutor 的接口一致 → ★可以直接替换试性能★
  ★ 任务函数和参数要能跨解释器传递(★规则类似 pickle★)

隔离性带来的实际差异(★容易踩坑★):
  ① 每个解释器要★重新 import★所有模块 → 启动慢、内存翻倍
  ② 模块级的全局变量、缓存、连接池★各是各的★
     → 「我在主解释器里初始化好了配置」在子解释器里★不存在★
  ③ 猴子补丁、注册表(如 logging 配置、signal handler)不共享
  ④ ★signal 只能在主解释器的主线程处理★
  ⑤ 有些模块在子解释器里会直接报错(见下一节)

API 层面,concurrent.interpreters(3.14) 提供 create()/exec()/call()/close() 等基本操作,而最实用的入口是 InterpreterPoolExecutor——它和 ThreadPoolExecutor/ProcessPoolExecutor 接口完全一致,可以直接替换来对比性能数据传递是关键限制:每个解释器有自己的堆和引用计数,一个对象的指针在另一个解释器里无效,所以不能直接传对象。可传的只有几类:「可共享类型」None/bool/int/float/bytes/str/tuple,以及能真正零拷贝的 memoryview)、interpreters.Queue(简单对象直传、复杂对象会被序列化)、以及 shared_memory(大数组的正确传法)。隔离性也会带来几个实际的坑:每个解释器要重新 import 所有模块(启动慢、内存翻倍)、模块级全局变量和连接池各是各的(「我在主解释器里初始化好了」在子解释器里不存在)、猴子补丁和 logging 配置不共享、信号只能在主解释器的主线程处理

四、限制与现实障碍

★ 障碍一:C 扩展兼容性(最大的现实约束)★
  传统 C 扩展的写法:
    static PyObject *cached_thing;        // ★进程级静态变量★
    static int initialized = 0;
  → 多个子解释器共用这一份静态状态
  → 后果:状态互相覆盖、类型对象错乱、引用计数跨解释器出错、★段错误★

  要安全支持子解释器,扩展必须:
    ① ★PEP 489 多阶段初始化★(multi-phase init)
    ② ★PEP 630 隔离模块状态★(把全局变量挪进 module state)
    ③ 声明 Py_mod_multiple_interpreters = Py_MOD_PER_INTERPRETER_GIL_SUPPORTED
  → 未声明的扩展在子解释器里 import 会直接报错:
    ImportError: module xxx does not support loading in subinterpreters

  生态现状(2026):
    已适配:部分标准库扩展、少量现代 C 扩展
    ★未适配:numpy、pandas、pytorch 等大量重量级库★
  → ★这是子解释器目前最大的实用性障碍★

★ 障碍二:不共享 = 数据传递成本★
  想在解释器间传一个 1GB 的 DataFrame?
  → 要么序列化(和多进程一样慢)
  → 要么用 shared_memory(要自己管理,且只适合数组类数据)
  → ★"共享内存空间"并不等于"能直接共享 Python 对象"★

★ 障碍三:崩溃不隔离★
  子解释器仍在★同一个进程★里:
  → 一个解释器里的段错误 / OOM / os._exit() ★会杀死整个进程★
  → 隔离的是"Python 层的命名空间",★不是操作系统层的故障域★
  → 需要真正的故障隔离,还是得用多进程

★ 障碍四:启动开销★
  创建一个子解释器要初始化整套解释器状态 + 重新 import
    线程:      ~50μs
    ★子解释器:~1~10ms★(依赖 import 的模块数量)
    进程(fork): ~1ms
    进程(spawn):~50~200ms
  → ★不适合频繁创建销毁★,应该池化复用

★ 障碍五:某些功能受限★
  - signal:只有主解释器的主线程能处理
  - 部分标准库模块在子解释器里行为受限
  - 调试和 profiling 工具支持还不完善
  - 错误信息跨解释器传递时会退化(ExecutionFailed 包一层)

★ 成熟度判断(2026 年):
  可以做什么:实验、评估、写不依赖重量级 C 扩展的纯 Python 并行任务
  不建议做什么:★生产环境的核心路径★(生态适配还很早期)

子解释器目前有五个现实障碍。最大的是 C 扩展兼容性:传统 C 扩展用进程级静态变量保存状态,多个子解释器共用这一份会导致状态互相覆盖、类型对象错乱、甚至段错误;要安全支持必须改造成 PEP 489 多阶段初始化 + PEP 630 隔离模块状态并显式声明支持,未适配的扩展 import 时会直接报错——而 numpy、pandas、pytorch 等重量级库目前都还没适配,这是实用性的主要瓶颈。第二个障碍是「不共享」带来的数据传递成本——「共享内存空间」并不等于「能直接共享 Python 对象」,传大对象要么序列化(和多进程一样慢)要么用 shared_memory第三个是崩溃不隔离:子解释器仍在同一进程里,一个解释器的段错误或 OOM 会杀死整个进程——它隔离的是「Python 层命名空间」而不是操作系统层的故障域第四是启动开销(约 1~10ms,比线程重得多,必须池化复用)。第五是功能受限(信号只能在主解释器主线程处理、调试工具支持不完善)。

五、什么时候值得用

★ 子解释器的独特价值:★隔离 + 比进程轻★

  场景一:★插件/多租户隔离★(最经典的用途)
    一个进程里跑多个互不信任的插件/用户脚本
    → 各自的 sys.modules、猴子补丁、全局变量完全隔离
    → 一个插件改了 json.dumps,不会影响别的插件
    → mod_wsgi 用子解释器隔离不同 Web 应用就是这个思路
    ★ 但注意:★不是安全沙箱★(同进程可以通过 ctypes 等突破)

  场景二:★CPU 并行 + 不需要共享大数据★
    每个任务自带输入、输出也不大(如批量计算、渲染、编解码)
    → 比多进程启动快、比多线程能真并行
    → InterpreterPoolExecutor 直接替换 ProcessPoolExecutor 试试

  场景三:★避免全局状态冲突★
    同一个库需要用不同配置跑多份(如不同的 locale、不同的随机种子、
    不同版本的配置单例)
    → 每个解释器一份,互不干扰

  场景四:★嵌入式 / 宿主程序★(C 层的经典用法)
    一个 C/C++ 应用嵌入 Python,要为每个请求/每个用户开一个干净环境

★ 不适合的场景:
  ✗ 需要共享大对象 → ★自由线程或 shared_memory★
  ✗ 依赖 numpy/pandas 等未适配的 C 扩展 → ★用多进程★
  ✗ 需要真正的故障隔离(防止崩溃拖垮全局)→ ★多进程★
  ✗ IO 密集 → ★asyncio 或线程★(子解释器没有优势)
  ✗ 生产核心路径(2026 年的成熟度)→ 再等等

★ 与其他方案的选型表:
  ┌──────────────────────────────┬────────────────────────┐
  │ 需求                          │ 首选                    │
  ├──────────────────────────────┼────────────────────────┤
  │ IO 密集、海量并发              │ ★asyncio★              │
  │ IO 密集、用同步库              │ 线程池                  │
  │ CPU 密集、要共享大数据         │ ★自由线程(3.13t)/共享内存★│
  │ CPU 密集、任务独立、要故障隔离  │ ★多进程★               │
  │ CPU 密集、任务独立、要轻量+隔离 │ ★子解释器★             │
  │ 插件/多租户的命名空间隔离       │ ★子解释器★             │
  └──────────────────────────────┴────────────────────────┘

面试怎么答(★要点式★):
  "子解释器是在一个进程里创建多个独立的 Python 解释器实例,
   每个有自己的 sys.modules、类型对象和★自己的 GIL(PEP 684,3.12)★,
   因此可以在多核上真正并行。★PEP 734 在 3.13/3.14 提供了官方 API★
   (concurrent.interpreters + InterpreterPoolExecutor)。
   它和自由线程是★两条不同的路线★:子解释器不共享对象、靠 Queue 通信、
   胜在隔离,本质是『更轻的进程』;自由线程共享对象、胜在效率,本质是『真线程』。
   ★主要限制是 C 扩展要适配(PEP 489/630),numpy 等还不支持★,
   而且崩溃不隔离(同一进程),启动也比线程重(毫秒级)。"

子解释器的独特价值是**「隔离 + 比进程轻」。最经典的场景是插件/多租户隔离**(一个进程跑多个互不信任的脚本,各自的模块表和猴子补丁完全隔离——mod_wsgi 就是这么隔离不同 Web 应用的;但要强调它不是安全沙箱,同进程可以通过 ctypes 突破);其次是 CPU 并行且不需要共享大数据的任务(InterpreterPoolExecutor 可以直接替换 ProcessPoolExecutor 试性能);以及避免全局状态冲突(同一个库用不同配置跑多份)。不适合的场景很明确:需要共享大对象(用自由线程或共享内存)、依赖 numpy 等未适配的 C 扩展(用多进程)、需要真正的故障隔离(用多进程,因为子解释器崩溃会拖垮整个进程)、以及 IO 密集(asyncio 更合适)。

六、未来与实践建议

★ 三条路线的并存关系(不是互相取代):
  ┌────────────────────────────────────────────────────────────┐
  │  多进程          ── 最强隔离,最大开销                        │
  │  ★子解释器★      ── 中等隔离,中等开销   ← 填补了中间的空白    │
  │  ★自由线程★      ── 无隔离,最小开销                          │
  │  asyncio        ── 单线程内的并发(正交于以上三者)            │
  └────────────────────────────────────────────────────────────┘
  → CPython 的方向是"★把这几种模型都提供好,让用户按场景选★"

  ★ 有意思的组合:子解释器 + 自由线程
    未来可能出现"用子解释器做隔离单元、单元内用无 GIL 线程并行"的架构

现在(2026)该怎么做:
  ① ★了解概念、能说清区别★(面试高频的"新特性"考点)
  ② 想尝试:用 3.14 的 InterpreterPoolExecutor 跑纯 Python 的 CPU 任务
     → 和 ProcessPoolExecutor 对比启动时间和吞吐
  ③ ★不要在生产核心路径用★(C 扩展生态还很早期)
  ④ 写库的人:了解 PEP 489/630,新写的 C 扩展就按隔离模块状态的方式写
     → 顺便也是"自由线程友好"的基础

如何验证一个库是否支持:
  interp = interpreters.create()
  try:
      interp.exec("import 你的库")
      print("支持")
  except Exception as e:
      print("不支持:", e)

★ 常被问到的"它会取代什么吗":
  取代多进程? ★不会★——多进程的故障隔离和 C 扩展兼容性无可替代
  取代多线程? ★不会★——线程共享对象的效率是子解释器给不了的
  取代 asyncio?★不会★——正交的东西(asyncio 解决的是并发连接数)
  → ★它填补的是"想要隔离但不想付进程的代价"这个空白★

一句话总结定位:
  ★子解释器 = 进程的隔离性 + 接近线程的轻量 - 对象共享★
  代价是:C 扩展要适配、崩溃不隔离、数据要传递

三条路线是并存而不是互相取代的关系:多进程(最强隔离、最大开销)→ 子解释器(中等隔离、中等开销)→ 自由线程(无隔离、最小开销),而 asyncio 与它们正交。CPython 的方向是「把这几种模型都提供好,让用户按场景选」,未来甚至可能出现「用子解释器做隔离单元、单元内用无 GIL 线程并行」的组合架构。现在(2026 年)的务实建议是:了解概念并能说清区别(这是面试高频的新特性考点)、想尝试就用 3.14 的 InterpreterPoolExecutor 跑纯 Python 的 CPU 任务并和 ProcessPoolExecutor 对比、不要在生产核心路径使用(C 扩展生态还很早期);写 C 扩展的人则应该了解 PEP 489/630,按隔离模块状态的方式写新扩展(顺便也是自由线程友好的基础)。最后用一句话总结定位:子解释器 = 进程的隔离性 + 接近线程的轻量 − 对象共享

记忆钩子:「子解释器 = ★在同一个进程里创建多个独立的 Python 解释器实例★,每个有自己的 sys.modules、内置类型对象和★自己的 GIL★。三个 PEP 要理清:★PEP 684(3.12)实现 per-interpreter GIL★(把进程唯一的 GIL 拆成每解释器一把,这是真正能并行的关键,但只有 C API)→ PEP 734 提供 Python 层 API(3.13 实验性的 test.support.interpreters、★3.14 正式的 concurrent.interpreters + InterpreterPoolExecutor★)→ PEP 554 是被取代的早期草案。★最重要的区分:子解释器 ≠ 自由线程★,它们是两条独立路线——★子解释器是『一个进程、多个解释器、各有 GIL、对象不共享、胜在隔离』,本质是「更轻的进程」;自由线程是『一个解释器、多线程、没有 GIL、对象直接共享、胜在效率』,本质是「真线程」★。数据传递是硬限制:每个解释器有自己的堆和引用计数,★不能直接传对象★,只能传可共享类型(None/bool/int/float/bytes/str/tuple、★memoryview 才是真零拷贝★)或走 interpreters.Queue(复杂对象会序列化)。五个现实障碍:①★C 扩展必须适配 PEP 489 多阶段初始化 + PEP 630 隔离模块状态★,未适配的 import 就报错,而 ★numpy/pandas 等目前都还不支持★——这是最大瓶颈;②不共享意味着传大数据仍然要序列化或用 shared_memory;③★崩溃不隔离★(同一进程,段错误会杀死全部,隔离的是命名空间不是故障域);④启动约 ★1~10ms★(比线程的微秒级重,必须池化);⑤signal 只能在主解释器主线程处理。定位一句话:★进程的隔离性 + 接近线程的轻量 − 对象共享★;经典用途是插件/多租户隔离(但★不是安全沙箱★)和『CPU 并行且不需共享大数据』的任务。三条路线是并存关系:多进程(最强隔离)→ 子解释器(中等)→ 自由线程(最轻),asyncio 与它们正交。」

七、常见误区与追问

  • 误区:子解释器就是「没有 GIL 的 Python」。 恰恰相反——每个子解释器都有自己的一把 GIL(PEP 684),并行是靠「多把锁各管一摊」实现的,而不是「取消锁」。这和自由线程(PEP 703,真正移除 GIL)是两条完全不同的技术路线子解释器里,同一个解释器内部的多个线程仍然要抢那个解释器的 GIL,所以在一个子解释器里开多线程做 CPU 计算依然不会加速;并行发生在解释器之间。理解这一点才能正确使用它:并行的单位是「解释器」而不是「线程」,所以你需要创建多个子解释器(通常用 InterpreterPoolExecutor 池化),而不是在一个子解释器里开多线程。
  • 误区:子解释器在同一个进程里,所以可以直接共享 Python 对象,比多进程高效得多。 共享内存空间 ≠ 能共享 Python 对象。每个解释器有自己的堆和引用计数体系——把一个对象的指针交给另一个解释器,它的引用计数会被两边同时修改(各自的 GIL 保护不了对方),导致提前释放或永不释放,是必然崩溃的用法。所以 PEP 734 只允许传递**「可共享的类型」None/bool/int/float/bytes/str/tuple,以及能真正零拷贝的 memoryview),复杂对象通过 interpreters.Queue 传递时会被序列化**——这一点的开销和多进程是同一量级。真正想零拷贝共享大数组,仍然要用 memoryview/shared_memory 这类「数据不在 Python 对象里」的方案。
  • 误区:子解释器提供了故障隔离,一个解释器崩了不影响其他的。 它隔离的是 Python 层的命名空间sys.modules、全局变量、猴子补丁、类型对象),不是操作系统层的故障域。所有子解释器共享同一个进程,因此:C 扩展里的段错误会直接杀死整个进程;某个解释器申请内存过多触发 OOM Killer,被杀的是整个进程;有人调用 os._exit()os.abort(),全部解释器一起消失;甚至一个解释器把进程的文件描述符耗尽,其他解释器也会跟着失败。需要真正的故障隔离(一个任务崩了不影响别人)只能用多进程——这也是多进程至今不可替代的核心价值。
  • 误区:子解释器可以当作安全沙箱来运行不可信代码。 不能。命名空间隔离只是「默认互不可见」,不是「强制不可达」——运行在子解释器里的代码依然是同一个进程里的原生 Python:它可以 import ctypes 直接读写进程内存、可以 import os 执行系统调用和读写文件、可以打开网络连接、可以调用 os._exit() 干掉整个进程。历史上 Python 层面的沙箱尝试(rexecBastion)都因为无法堵住所有逃逸路径而被移除。运行不可信代码的正确方案是操作系统级或更强的隔离:独立进程 + seccomp/capabilities 限制、容器、gVisor/Firecracker 这类沙箱运行时、或者 WebAssembly。子解释器的隔离价值在于防止「善意但互相干扰」的代码打架(插件改了全局配置、猴子补丁冲突),而不是防御恶意代码。
  • 误区:import numpy 在子解释器里应该没问题,反正它很成熟。 恰恰是成熟的重量级 C 扩展最难支持。传统 C 扩展把状态存在进程级的 static 变量里(缓存的类型对象、初始化标志、全局单例),多个子解释器共用这一份静态状态会导致互相覆盖、类型对象错乱、引用计数跨解释器出错,轻则行为诡异、重则段错误。要安全支持必须做两件事:PEP 489 多阶段初始化(让模块能被多次独立初始化)和 PEP 630 隔离模块状态(把全局变量搬进 per-module state),并显式声明 Py_MOD_PER_INTERPRETER_GIL_SUPPORTED未声明的扩展在子解释器里 import 会直接抛 ImportError(这是保护机制,好过让它崩溃)。截至目前 numpy、pandas、pytorch 等主流科学计算库尚未完成适配——这正是子解释器还停留在「前沿特性」阶段的主要原因。
  • 追问:PEP 554、684、734 分别是什么,关系是怎样的? PEP 554(2017) 最早提出把子解释器暴露成 Python 层的 interpreters 模块,但它长期停留在草案状态——因为当时所有子解释器共用一把 GIL,暴露出去也只有隔离、没有并行,价值有限。PEP 684(Python 3.12 实现) 才是关键突破:per-interpreter GIL,把原本进程唯一的 GIL 拆成「每个解释器一把」,这要求 CPython 把大量进程级全局状态(内置类型对象、小整数缓存、interned 字符串池、内存分配器 arena)改造成解释器级——从此多个子解释器可以在多核上真正并行;但 3.12 只提供了 C API,Python 代码还用不了。PEP 734 接替 PEP 554 成为官方的 Python 层 API 提案,3.13 以 test.support.interpreters 的形式实验性提供,3.14 正式成为 concurrent.interpreters,并配套了 concurrent.futures.InterpreterPoolExecutor。记忆顺序是:3.12 打通底层(各自的 GIL)→ 3.13 实验性 API → 3.14 正式模块
  • 追问:什么场景下子解释器比多进程更合适? 三个特征同时成立时它最有价值:① 需要隔离但不需要故障隔离——比如插件系统、多租户脚本执行、同一个库用不同配置跑多份,你要的是「全局状态、猴子补丁、模块配置互不干扰」,而不是「一个崩了别人还活着」;② 创建/销毁较频繁或数量较多——子解释器启动约 110ms,而 spawn 进程要 50200ms,内存占用也小得多(不用复制整个进程);③ 任务间传递的数据量小——因为跨解释器传复杂对象仍然要序列化,数据量大时它相对多进程没有优势。反过来,只要需要真正的故障隔离(防止段错误拖垮全局)、依赖未适配的 C 扩展(numpy/pandas)、或者要共享大块数据,多进程(配 shared_memory)仍然是更实际的选择。一个典型的正面例子是嵌入式宿主程序:C/C++ 应用嵌入 Python,为每个请求开一个干净的执行环境——这也是 Py_NewInterpreter() 这个 C API 从 1997 年就存在的原始用途。
  • 追问:子解释器和自由线程(no-GIL)会互相取代吗? 不会,它们解决的是不同维度的问题,未来会并存甚至组合使用自由线程去掉了 GIL,让同一个解释器内的多个线程直接共享对象并真正并行——优势是零拷贝共享(处理几 GB 的数据不用序列化),代价是竞态完全暴露(所有共享可变状态必须自己加锁)和单线程性能下降 5~10%子解释器保留了 GIL 但让每个解释器各有一把——优势是强隔离(不共享就没有竞态、全局状态互不污染),代价是对象要传递每个解释器的内存与启动开销。所以选择取决于「你更需要共享还是更需要隔离」:共享大数据、任务间耦合紧 → 自由线程;插件隔离、多租户、避免全局状态冲突 → 子解释器;要故障隔离 → 多进程。CPython 的整体方向是把这几种模型都提供好,未来甚至可能出现「用子解释器做隔离单元、单元内用无 GIL 线程并行」的组合架构。

八、加强记忆

子解释器是在同一个进程里创建多个独立的 Python 解释器实例,每个有自己的 sys.modules、内置类型对象和自己的 GIL。三个 PEP 的关系要理清:PEP 684(3.12)实现了 per-interpreter GIL——把进程唯一的 GIL 拆成每个解释器一把,这是真正能并行的关键一步(但只有 C API);PEP 734 提供 Python 层 API(3.13 实验性的 test.support.interpreters3.14 正式的 concurrent.interpreters + InterpreterPoolExecutor);PEP 554 是被取代的早期草案。最重要的区分:子解释器 ≠ 自由线程,二者是两条独立路线——子解释器是「一个进程、多个解释器、各有 GIL、对象不共享、胜在隔离」,本质是「更轻的进程」;自由线程是「一个解释器、多线程、没有 GIL、对象直接共享、胜在效率」,本质是「真线程」。数据传递是硬限制:每个解释器有自己的堆和引用计数,不能直接传对象,只能传可共享类型(None/bool/int/float/bytes/str/tuple,其中 memoryview 才是真正的零拷贝)或走 interpreters.Queue(复杂对象会被序列化)。五个现实障碍:① C 扩展必须适配 PEP 489 多阶段初始化 + PEP 630 隔离模块状态,未适配的 import 直接报错,而 numpy/pandas 等目前都还不支持——这是最大瓶颈;② 不共享意味着传大数据仍要序列化或用 shared_memory;③ 崩溃不隔离(同一进程,段错误会杀死全部;隔离的是命名空间而非故障域,也不是安全沙箱);④ 启动约 1~10ms(比线程的微秒级重得多,必须池化复用);⑤ 信号只能在主解释器的主线程处理。定位一句话:进程的隔离性 + 接近线程的轻量 − 对象共享;经典用途是插件/多租户隔离和「CPU 并行且不需共享大数据」的任务。最后记住三条路线是并存关系:多进程(最强隔离、最大开销)→ 子解释器(中等)→ 自由线程(最轻、无隔离),而 asyncio 与它们正交。