想让 Python 代码真正并行,除了多进程,3.14 又多了一条路:同一个进程里跑多个相互隔离的解释器,每个有自己的 GIL。这篇用标准库的 concurrent.interpreters 和 InterpreterPoolExecutor 实际跑一遍:隔离到什么程度、能传什么、快多少、启动贵不贵。
main = Interpreter(0), interpreters.create() 用时 8.39 ms → Interpreter(1) list_all(): [Interpreter(0), Interpreter(1)] 子解释器里定义的 Y, 主解释器能看到吗? False 子解释器给 sys 加的属性, 主解释器的 sys 有吗? False 子解释器看得到主解释器的全局变量 X 吗? False sys.modules 是各自独立的: 主解释器 len = 135 | 子解释器 len = 52
子解释器里定义的变量、给 sys 加的属性,主解释器都看不到,反过来主解释器的全局变量它也看不到;sys.modules 是各自独立的一份(主 135 个模块,子解释器刚创建只有 52 个)。它们在同一个进程、同一个地址空间里,但语言层面互不可见——比线程隔离得多,又比进程轻得多。exec 出错时抛 ExecutionFailed,而且出错之后那个子解释器仍然能继续用。
Interpreter.call(func, arg) 的参数与返回值 it.call(echo, int 42 ) → 成功, 返回 42 (is_shareable=True)
it.call(echo, str ) → 成功, 返回 'hello' (is_shareable=True)
it.call(echo, bytes ) → 成功, 返回 b'ab' (is_shareable=True)
it.call(echo, tuple (1, 'a') ) → 成功, 返回 (1, 'a') (is_shareable=True)
it.call(echo, None ) → 成功, 返回 None (is_shareable=True)
it.call(echo, list [1, 2] ) → 成功, 返回 [1, 2] (is_shareable=False)
it.call(echo, dict ) → 成功, 返回 {'a': 1} (is_shareable=False)
it.call(echo, object() ) → 成功, 返回 <object object at 0x109554940> (is_shareable=False)
it.call(echo, lambda ) → 成功, 返回 <function main.<locals>.<lambda> at 0x109a29850> (is_shareable=False)
it.call(echo, 主脚本里定义的类 Foo() ) → NotShareableError: (<__main__.main.<locals>.Foo object at 0x109a341a0>,) does not support
it.call(echo, 模块里定义的类 Bar() ) → 成功, 返回 <w92mod.Bar object at 0x1099534d0> (is_shareable=False)
it.call(echo, threading.Lock()) → NotShareableError: (<unlocked _thread.lock object at 0x109a2e490>,) does not support cros
it.call(echo, 打开的文件 ) → NotShareableError: (<_io.TextIOWrapper name='/dev/null' mode='r' encoding='UTF-8'>,) does基本类型(int、str、bytes、tuple、None)是「可共享」的;list、dict 这类虽然 is_shareable 是 False,通过 call 也能传过去(按值复制)。真正传不了的是:线程锁、打开的文件——它们绑定着解释器/系统资源。有意思的是类的实例:定义在可导入模块里的 Bar() 能传过去,而定义在主脚本里的 Foo() 传不过去——因为对方解释器要重新构造它,而它的 __main__ 里根本没有这个类。这和第 89 篇里 pickle 的问题是同一件事。
it.call(在 main() 里定义的 local_fn) → 成功 1
it.call(w92mod.noop)(定义在可导入模块里) → 1
子解释器 exec('print(work)') → ExecutionFailed: name 'work' is not defined子解释器的 __main__ 是一个空的命名空间——主脚本里定义的 work,在子解释器里 exec("print(work)") 是 NameError 级别的失败。让子解释器执行你的函数,函数要放在它能 import 的模块里(我这里放进 w92mod.py)。我一开始把 work 定义在主脚本里再交给 InterpreterPoolExecutor,就是因为这个原因失败的。另外 ProcessPoolExecutor(macOS 默认 spawn)会重新导入主脚本,所以入口必须有 if __name__ == "__main__":——我第一次没写,结果整个脚本被重复执行了一遍。
串行做 4 份: 397.1 ms ThreadPoolExecutor 4 个 worker 做 4 份: 394.4 ms → 相对串行加速 1.01x InterpreterPoolExecutor 4 个 worker 做 4 份: 138.9 ms → 相对串行加速 2.86x ProcessPoolExecutor 4 个 worker 做 4 份: 179.1 ms → 相对串行加速 2.22x
串行做 4 份要 397 ms。线程池 394 ms,几乎没有加速——它们共享一个 GIL。子解释器池 139 ms,加速 2.86x:每个子解释器有自己独立的 GIL,所以可以同时在多个核上跑纯 Python 字节码,而且不需要 3.14t 那样的自由线程构建。进程池 179 ms(2.22x),这次比子解释器慢(spawn 一个进程的成本见验证五)。加速比不是线性的,这是本机 4 个 worker、这个特定任务下的结果。
create+call+close 一个子解释器 8.71 ms 创建并等待一个线程 0.04 ms spawn 一个进程(macOS 默认) 48.00 ms
线程创建约 0.04 ms,子解释器约 8.7 ms,spawn 一个进程约 48 ms。子解释器介于两者之间:比进程便宜得多,又比线程贵两百倍左右。所以它适合「少量、长期存在的 worker」,而不是「每个任务临时创建一个」。
concurrent.interpreters、concurrent.futures.InterpreterPoolExecutor;每项取 3 次最小值