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

Added in version 3.14.

源代码: Lib/concurrent/interpreters


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

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

For concurrency, interpreters themselves (and this module) don't provide much more than isolation, which on its own isn't useful. Actual concurrency is available separately through threads See below

参见

InterpreterPoolExecutor

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

隔离扩展模块

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

PEP 554

PEP 734

PEP 684

适用范围: not WASI.

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

关键细节

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

  • isolated, by default

  • 没有隐式线程

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

概述

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

The concept and functionality of the interpreter have been a part of Python since version 2.2, but the feature was only available through the C-API and not well known, and the isolation was relatively incomplete until version 3.12.

多解释器与隔离

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

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

备注

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

在一个解释器中运行

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

No other threads are automatically started for the operation. There is a helper for that though. There is another dedicated helper for calling the builtin exec() in an interpreter.

当在解释器中调用 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()

如果解释器当前正在执行其 __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()