一个进程里运行两个解释器,能否让 tasklab 的两个任务彼此不共享对象,同时避免起两个进程?在冻结的 CPython 3.12.3 上,公开标准库还没有可依赖的高级解释器池入口;实验只能明确使用当前实际存在的私有 _xxsubinterpreters 模块。它暴露 create/run_string/destroy,但本机构建里没有 channel_send 等通道 API。没有通道,就要先交代输入输出怎样过边界,不能用进程池的名义代替子解释器实验。

这项选修不改 30 的进程策略 或 29 的线程池。local、线程、进程和子解释器四种执行方式都从当前源码树导入同一份 tasklab,对 "one two one " * 50 计算已有 cpu_work(iterations=120),并提交、执行、查询词频任务。各自看到相同业务结果 {"one":100,"two":50} 和校验值 754902074;相同结果不代表存储空间相同。

哪些东西能越过解释器边界

在当前私有模块里 is_shareable(b"...") 为真,列表为假。实验用不可变 UTF-8 bytes 传入子解释器,子解释器创建自己的内存仓储、执行原有词频合同,再用 os.pipe 将少量 JSON 结果写回父侧;文件描述符属于同一进程的 OS 资源,这只是本次实验的输出通道,不是 Python 对象共享或一般意义的进程隔离。另在子解释器的 builtins 增加标记,父解释器没有这个属性;两者报告相同 PID。进程池的 worker 是另一个 PID,参数/结果经过序列化;普通线程在同一进程里可以直接拿到父侧仓储,实测线程处理前 queued,处理后父侧查询得到 succeeded。

1
2
3
父解释器 store ── 同一引用 ─→ 线程任务 → 父侧可见 succeeded
父解释器 bytes ── 可共享入参 ─→ 子解释器私有 store ─→ pipe(JSON) → 父侧观察结果
父解释器 str ── 序列化参数 ─→ 另一个进程的 store ─→ 序列化返回

同名 task-0001 在不同仓储里只是各自的序号,不能据此断定是同一项任务,更不能在两个模式之间复用任务提交确认。对于 SQLite 或文件等进程级共享资源,解释器间不共享 Python 对象也不能代替互斥或事务。

扩展加载和计时需要分开验

将 experiments/E07_reject.c 编译成只支持主解释器加载的 _rejectinterp,再在子解释器中按绝对路径单独导入,实测 RunFailedError 包装 ImportError: module _rejectinterp does not support loading in subinterpreters;没有跳过扩展检查而声称插件兼容。这里仅核对这份 .so 与这个构建,不能推广为所有 C 扩展都不支持子解释器。38 的 free-threaded 构建另有 ABI 与运行时 GIL 测试;这次没有配置独立解释器 GIL、没有验证并行 CPU 增益,也不因使用子解释器就宣称摆脱 GIL。

四种模式先各跑 1 次 warmup 并校验,再保存 5 次完整调用的 perf_counter_ns() 样本;每个样本都断言业务结果、checksum、隔离 marker 和扩展拒绝负例。当前封装脚本运行中 local 为 2.572–3.105 ms,线程为 5.590–5.981 ms,进程为 380.312–961.010 ms,子解释器为 58.460–175.578 ms。这里把调度、pipe/序列化、词频和 CPU 工作负载一起计时;进程 worker 首次启动并未预热,子解释器创建和执行也在同一段计时里,四种模式的样本不是公平的纯计算对比,更不能拿它排解释器速度榜。固定的基线仍以 36 的窄范围函数实验 为准。

验收与练习

experiments/E07_subinterpreters.py 从 CPython 3.12.3 macOS arm64 运行,保留具体扩展路径、异常原文、五次时长和 PID/对象共享断言;tests/test_optional_native.py 将这个负例放在可选原生测试里,无 _xxsubinterpreters 时跳过,不能借普通扩展在主解释器能导入替代。私有模块 API 可变,换版本须先重验模块符号;子解释器不是恶意代码沙箱,不能让不可信请求传入 run_string 执行任意代码。

预测题。 线程和子解释器各自调用 submit 后都产生 task-0001,能否用父侧 query 找到子解释器中的任务?答案:不能。父侧保存的是自己创建的仓储;子解释器的 dict 与 Task 对象在另一个解释器里。这里只有受约束的 bytes 输入和 pipe 上的 JSON 输出;父侧不持有子任务对象。

变更题。 把 bytes 入参换成 list[bytes],并把只声明主解释器支持的扩展 .so 路径原样导入子解释器,哪两个假设会失败?答案:本机构建 is_shareable([payload]) 返回假,run_string 不能据此共享可变列表;当前 _rejectinterp 在子解释器导入报 ImportError。可分别观察 is_shareable 输出和 extension 的 loaded:false/error_type;不能从列表失败推断字符串、不可变字节或所有扩展都会失败。

参考资料

  • Python 3.12 C API:Sub-interpreter support、PEP 684:每解释器 GIL:实现配置与共享边界;本篇使用的 Python 层模块是实际发行版中的私有 API,不把 C API 选项当成本次已开启的 GIL 模式。
  • examples/python-language/experiments/E07_subinterpreters.py、examples/python-language/experiments/E07_reject.c 与 examples/python-language/evidence/completion/native/:同源工作负载、通道、扩展加载结果、五次原始样本。