concurrent.interpreters --- 同一进程中的多个解释器

Added in version 3.14.

源代码: Lib/concurrent/interpreters


concurrent.interpreters 模块在低层级的 _interpreters 模块之上构造了更高层级的接口。

本模块的最初目标是提供一个基本 API 来管理解释器(也称“子解释器”)并在其中运行任务。 运行过程涉及(在当前线程中)切换解释器并在执行上下文中调用函数。

对于并发操作,解释器本身(以及本模块)并未提供状态隔离以外的更多手段,对其自身而言用处不大。 实际的并发是通过 线程 单独提供的。 参见 below

参见

InterpreterPoolExecutor

通过熟悉的接口实现线程与解释器的结合。

隔离扩展模块

如何将扩展模块更新为支持多解释器

PEP 554

PEP 734

PEP 684

Availability: not WASI.

此模块在 WebAssembly 平台上无效或不可用。 请参阅 WebAssembly 平台 了解详情。

关键细节

在进一步深入之前,关于多解释器的使用需要注意以下几个要点:

  • 默认情况下为 解释器隔离

  • 没有隐式线程

  • 并不是所有的PyPI包都支持在多个解释器中使用

概述

"解释器"本质上是 Python 运行时的执行上下文,它包含了运行时执行程序所需的所有状态,包括导入状态和内置对象等。(每个线程——即使只有主线程——除了当前解释器外,还拥有一些额外的运行时状态,这些状态与当前异常和字节码求值循环相关。)

解释器的概念和功能自 Python 2.2 版本起便已存在,但该特性此前仅能通过 C-API 使用且鲜为人知,同时 隔离 功能在 3.12 版本前相对不够完善。

多解释器与隔离

Python 实现方案可能支持在同一进程中使用多个解释器,CPython 就具备此功能。每个解释器实际上都是相互隔离的(仅有少量经过严格管控的进程级全局例外情况)。

这种隔离机制的主要价值在于为程序的不同逻辑组件提供严格隔离,使开发者能够精准控制这些组件之间的交互方式。

备注

从技术上讲,同一进程中的解释器永远无法实现严格隔离,因为在同一进程内对内存访问几乎没有任何限制。Python 运行时会尽力确保隔离性,但扩展模块很容易破坏这种隔离。因此,在安全敏感场景下——当不同解释器之间本不应相互访问数据时——请勿使用多解释器模式。

在一个解释器中运行

在另一个解释器中运行涉及两个步骤:首先在当前线程切换至目标解释器,然后调用目标函数。运行时将基于当前解释器的状态执行该函数。concurrent.interpreters 模块提供了一套基础API,用于创建和管理解释器,以及执行这种"切换-调用"操作。

该操作不会自动启动其他线程,但可通过 一个辅助工具 实现此功能。此外还提供了专用辅助工具,用于在解释器中调用内置函数 exec()

当在解释器中调用 exec() (或 eval()) 时,它们会使用该解释器的 __main__ 模块作为"全局"命名空间来运行。对于未关联任何模块的函数也是如此。这与从命令行调用脚本时在 __main__ 模块中运行的方式相同。

并发与并行

如前所述,解释器本身并不提供任何并发能力。它们严格代表了运行时 在当前线程 中将使用的隔离执行上下文。这种隔离特性使解释器与进程相似,但同时又能像线程一样享受进程内的高效性。

综上所述,解释器确实天然支持某些形式的并发。这种隔离性带来了一个重要的特性:它支持一种不同于异步编程或线程模型的并发实现方式,其并发模型与CSP(通信顺序进程)或Actor模型相似——而这类模型通常更易于推理分析。

开发者可以在单线程中利用这种并发模型,以无栈式风格在解释器之间来回切换。然而,当将多解释器与多线程结合使用时,该模型才更能体现其价值。这种结合主要涉及:启动新线程 → 切换至目标解释器 → 执行所需操作。

在Python中,每个实际线程(即使仅运行主线程)都拥有自己的 当前 执行上下文。多个线程可以共享同一个解释器,也可以使用不同的解释器。

从高层次来看,可以将线程与解释器的组合理解为可选共享的线程模型。

一个重要优势是:解释器之间的隔离足够彻底,它们不共享 GIL,这意味着将多线程与多解释器结合使用时,可以实现真正的多核并行处理。(该特性自 Python 3.12 起支持。)

解释器间通信

在实际应用中,多解释器模式的价值取决于是否存在有效的通信机制。通常采用消息传递方式实现交互,但在严格管控条件下也可共享数据。

基于此,concurrent.interpreters 模块提供了通过 create_queue() 访问的 queue.Queue 实现。

"共享"对象

在解释器间实际共享的任何数据都会丧失由 GIL 提供的线程安全性。扩展模块可通过多种方案处理此问题,但在纯Python代码中,由于缺乏线程安全机制,对象实际上无法真正共享(仅有少数例外)。这种情况下必须创建对象副本,意味着可变对象无法保持同步状态。

默认情况下,当对象传递给其他解释器时,多数对象会通过 pickle 模块进行复制。几乎所有不可变内置对象要么直接共享,要么会高效复制。例如:

仅有少数Python类型能够真正在解释器间共享可变数据:

参考

这个模块定义了以下函数:

concurrent.interpreters.list_all()

返回一个 Interpreter 对象的 list,每个对象对应一个现有的解释器。

concurrent.interpreters.get_current()

为当前运行的解释器返回一个 Interpreter 对象。

concurrent.interpreters.get_main()

返回主解释器的 Interpreter 对象。该解释器是运行时为执行 REPL 或命令行脚本而创建的,通常也是唯一存在的解释器实例。

concurrent.interpreters.create()

初始化一个新的(空闲的)Python解释器并为其返回一个 Interpreter 对象。

concurrent.interpreters.create_queue()

初始化一个新的跨解释器队列,并返回其对应的 Queue 对象。

解释器对象

class concurrent.interpreters.Interpreter(id)

当前进程中的单个解释器。

一般来说,不应该直接调用 Interpreter。 相反,使用 create() 或其他模块函数之一。

id

(只读)

底层解释器的 ID。

whence

(只读)

描述解释器来源的字符串。

is_running()

如果解释器当前正在执行其:mod:!__main__`模块中的代码,则返回``True`,否则返回``False``。

close()

完成和销毁解释器。

prepare_main(ns=None, **kwargs)

将对象绑定到解释器的 __main__ 模块中。

部分对象会实际共享,部分对象可高效复制,但大多数对象仍需通过 pickle 模块进行复制。具体参见 "共享"对象

exec(code, /, dedent=True)

在解释器中运行给定的源代码(在当前线程中)。

call(callable, /, *args, **kwargs)

返回在解释器中(在当前线程中)调用运行给定函数的结果。

call_in_thread(callable, /, *args, **kwargs)

在解释器中运行给定的函数(在一个新的线程中)。

异常

exception concurrent.interpreters.InterpreterError

此异常是 Exception 的子类,在发生解释器相关错误时引发。

exception concurrent.interpreters.InterpreterNotFoundError

此异常是 InterpreterError 的子类,当目标解释器不再存在时引发。

exception concurrent.interpreters.ExecutionFailed

此异常是 InterpreterError 的子类,当运行的代码引发未捕获的异常时引发。

excinfo

在其他解释器中引发的异常的基本快照。

exception concurrent.interpreters.NotShareableError

此异常是 TypeError 的子类,当一个对象无法发送到另一个解释器时引发。

解释器间通信

class concurrent.interpreters.Queue(id)

这是一个对底层跨解释器队列的封装,实现了标准的 queue.Queue 接口。底层队列只能通过 create_queue() 函数创建。

部分对象会实际共享,部分对象可高效复制,但大多数对象仍需通过 pickle 模块进行复制。具体参见 "共享"对象

id

(只读)

队列的ID。

exception concurrent.interpreters.QueueEmptyError

此异常继承自 queue.Empty,当队列为空时,会由 Queue.get()Queue.get_nowait() 方法引发。

exception concurrent.interpreters.QueueFullError

此异常继承自 queue.Full,当队列已满时,会由 Queue.put()Queue.put_nowait() 方法引发。

基本使用

创建一个解释器并在其中运行代码::

from concurrent import interpreters

interp = interpreters.create()

# 在当前操作系统线程中运行。

interp.exec('print("spam!")')

interp.exec("""if True:
    print('spam!')
    """)

from textwrap import dedent
interp.exec(dedent("""
    print('spam!')
    """))

def run(arg):
    return arg

res = interp.call(run, 'spam!')
print(res)

def run():
    print('spam!')

interp.call(run)

# 在新的操作系统线程中运行

t = interp.call_in_thread(run)
t.join()