注意
该 PEP 本质上是 PEP 554 的延续。该文件 在 7 年的讨论中增加了许多辅助信息。 此 PEP 是对基本信息的简化。大部分 这些额外的信息仍然有效和有用,只是不在 此处具体提案的直接上下文。
抽象
此 PEP 建议添加一个新模块,以支持 在 当前流程。这包括表示 基础解释器。该模块还将为口译员之间的交流提供基本课程。 最后,我们将添加一个新的基于模块。interpretersInterpreterQueueconcurrent.futures.InterpreterPoolExecutorinterpreters
介绍
从根本上说,“解释器”是(本质上)的集合 Python 线程必须共享的所有运行时状态。所以,让我们先来看看 看看线程。然后我们再回到口译员身上。
线程和线程状态
Python 进程将具有一个或多个运行 Python 代码的操作系统线程 (或以其他方式与 C API 交互)。这些线程中的每一个 使用自己的线程状态与 CPython 运行时交互 (),它保存所有唯一的运行时状态 线。还有一些运行时状态在 多个操作系统线程。PyThreadState
任何操作系统线程都可以切换它当前使用的线程状态,如 只要它不是另一个操作系统线程已经在使用(或已经 一直在使用)。此“当前”线程状态由运行时存储 在线程局部变量中,可以使用 显式查找。它会自动设置为初始 (“主要”)OS 线程和 for 对象。从 C API 它被设置(和清除)和 may 由 设置。大多数 C API 都要求 有一个当前线程状态,要么隐式查找 或作为参数传入。PyThreadState_Get()threading.ThreadPyThreadState_Swap()PyGILState_Ensure()
操作系统线程和线程状态之间的关系是一对多的。 每个线程状态最多与单个操作系统线程相关联,并且 记录其线程 ID。一个线程状态永远不会用于多个线程 操作系统线程。然而,在另一个方向上,操作系统线程可能具有更多 而不是一个与之关联的线程状态,但同样只有一个 可能是最新的。
当一个操作系统线程有多个线程状态时,用于该操作系统线程进行切换 在它们之间,请求的线程状态成为当前状态。 使用旧线程状态在线程中运行的任何内容都是 有效暂停,直到该线程状态被交换回。PyThreadState_Swap()
解释器状态
如前所述,存在一些运行时状态,即多个操作系统线程 共享。其中一些是由模块公开的,尽管很多是公开的 在内部使用,不显式公开或仅通过 C API 公开。sys
这种共享状态称为解释器状态 ().我们有时在这里将其称为 “interpreter”,尽管它有时也用于指代可执行文件、Python 实现和 字节码解释器(即 /)。PyInterpreterStatepythonexec()eval()
CPython 在同一进程中支持多个解释器(AKA “subinterpreters”),自 1.5 版(1997 年)起。该功能已 可通过 C API 获得。
解释器和线程
线程状态与解释器状态的关系大致相同 操作系统线程和进程是相关的(在高级别)。自 首先,这种关系是一对多的。 线程状态属于单个解释器(并存储 指向它的指针)。该线程状态永远不会用于不同的 译员。然而,在另一个方向上,口译员可能有 零个或多个与之关联的线程状态。解释器只有 在操作系统线程中被视为活动状态之一 是最新的。
解释器是通过 C API 使用 (or , which 是轻包)。 该函数执行以下操作:Py_NewInterpreterFromConfig()Py_NewInterpreter()Py_NewInterpreterFromConfig()
创建新的解释器状态
创建新的线程状态
将线程状态设置为当前 (解释器初始化需要当前状态)
使用该线程状态初始化解释器状态
返回线程状态(仍为当前状态)
请注意,返回的线程状态可能会立即被丢弃。 不要求解释器具有任何线程状态, 除非口译员是要实际使用的。 此时,它必须在当前操作系统线程中处于活动状态。
要使现有解释器在当前操作系统线程中处于活动状态, C API 用户首先确保解释器具有相应的 线程状态。然后像正常一样被称为 使用该线程状态。如果另一个解释器的线程状态 已经是最新的,然后像往常一样被换掉并执行 因此,OS 线程中的该解释器被有效地暂停,直到 它被换回。PyThreadState_Swap()
一旦解释器在当前操作系统线程中处于活动状态,就像这样, thread 可以调用任何 C API,例如 (即 )。这通过使用当前线程状态作为 运行时上下文。PyEval_EvalCode()exec()
“主要”口译员
当 Python 进程启动时,它会创建单个解释器状态 (“mAIn”解释器)具有当前单线程状态 操作系统线程。然后使用它们初始化 Python 运行时。
初始化后,脚本或模块或 REPL 使用 他们。该执行发生在解释器的模块中。__main__
当进程完成运行请求的 Python 代码或 REPL 时, 在主操作系统线程中,Python 运行时在该线程中完成 使用主解释器。
运行时终结对仍在运行只有轻微的间接影响 Python 线程,无论是在主解释器中还是在子解释器中。 那是因为它会立即无限期地等待所有非守护进程 Python 线程完成。
虽然可以查询 C API,但没有一种机制可以查询任何 Python 线程直接收到定稿已经开始的警报, 除了可能具有“atexit”功能之外,可能是 使用 注册。threading._register_atexit()
任何剩余的子解释器稍后自行完成, 但此时它们在任何操作系统线程中都不是最新的。
解释器隔离
CPython 的解释器旨在与每个 其他。这意味着解释器从不共享对象(除非在非常 具有不朽的、不可变的内置对象的特定情况)。每 解释器有自己的模块()、类、函数、 和变量。即使两个解释器定义同一个类, 每个都有自己的副本。这同样适用于 C 中的状态,包括 在扩展模块中。CPython C API 文档对此进行了更多说明。sys.modules
值得注意的是,解释器会有一些进程全局状态 总是共享,有些是可变的,有些是不可变的。共享不可变 状态几乎没有问题,同时提供了一些好处(主要是 性能)。但是,所有共享的可变状态都需要特殊的 管理,特别是线程安全,其中一些操作系统 照顾我们。
可变:
文件描述符
低级环境 VAR
进程内存(尽管分配器是隔离的)
口译员名单
变:
内置类型(例如 ,dictbytes)
单例(例如None)
底层静态模块数据(例如函数) 内置/扩展/冻结模块
现有执行组件
Python 的许多现有部分可能会有所帮助 了解如何在子解释器中运行代码。
在 CPython 中,每个组件都是围绕以下内容之一构建的 C API 函数(或变体):
PyEval_EvalCode():使用给定的字节码解释器运行字节码解释器 Code 对象
PyRun_String(): 编译 +PyEval_EvalCode()
PyRun_File():读取 + 编译 +PyEval_EvalCode()
PyRun_InteractiveOneObject(): 编译 +PyEval_EvalCode()
PyObject_Call():调用PyEval_EvalCode()
内置.exec()
内置函数可用于执行 Python 代码。是的 本质上是 C API 函数和 .exec()PyRun_String()PyEval_EvalCode()
以下是内置的一些相关特征:exec()
它在当前操作系统线程中运行并暂停任何内容 在那里运行,完成后恢复。 其他操作系统线程不受影响。 (为避免暂停当前 Python 线程,请在 .exec()exec()threading.Thread
它可能会启动其他线程,这些线程不会中断它。
它针对“globals”命名空间(和“locals”)执行 命名空间)。在模块级别,默认使用当前模块(即 )。 按原样使用该命名空间,并且不会在之前或之后清除它。exec()__dict__globals()exec()
它从它运行的代码中传播任何未捕获的异常。 异常是从 Python 中的调用引发的 最初称为 .exec()exec()
命令行
CLI 提供了多种运行 Python 代码的方法。在每个 案例 它映射到相应的 C API 调用:python
<no args>, - 运行 REPL (-iPyRun_InteractiveOneObject())
<filename>- 运行脚本 (PyRun_File())
-c <code>- 运行给定的 Python 代码 (PyRun_String())
-m module- 将模块作为脚本运行 ( 通过PyEval_EvalCode()runpy._run_module_as_main())
在每种情况下,它本质上都是在主解释器模块的顶层运行的变体。exec()__main__
线程。线
当 Python 线程启动时,它会运行“target”函数 使用新的线程状态。全球 命名空间来自和任何未捕获的 异常被丢弃。PyObject_Call()func.__globals__
赋予动机
该模块将提供一个高级接口,用于 多个解释器功能。目标是使现有的 CPython 的多解释器功能更易于访问 Python 代码。这一点尤其重要,因为 CPython 有一个 每个口译员 GIL (PEP 684) 和人们更感兴趣 在使用多个口译员时。interpreters
如果没有 stdlib 模块,用户只能使用 C API,这限制了多少 他们可以尝试并利用多个口译员。
该模块将包括一个用于在 口 译员。没有一个,多个口译员就少得多 有用的功能。
该模块将:
公开现有的多解释器支持
引入口译员之间通信的基本机制
该模块将包装一个新的低级模块 (与模块相同)。 但是,该低级 API 不供公众使用 因此不是本提案的一部分。_interpretersthreading
使用解释器
该模块定义了以下功能:
get_current() -> Interpreter
返回当前正在执行的对象 译员。Interpreter
list_all() -> list[Interpreter]
返回每个现有解释器的对象, 它当前是否在任何操作系统线程中运行。Interpreter
create() -> Interpreter
创建新的解释器并返回对象 为了它。解释器本身不做任何事情,而是 本质上不绑定到任何操作系统线程。这只有在以下情况下才会发生 某些东西实际上是在解释器中运行的 (例如),并且仅在运行时。 解释器可能有也可能没有准备好使用的线程状态, 但这严格来说是一个内部实现细节。InterpreterInterpreter.exec()
解释器对象
一个代表解释器的对象 () 替换为相应的唯一 ID。 任何给定的解释器都只有一个对象。interpreters.InterpreterPyInterpreterState
如果解释器是用 then 创建的 一旦所有物体都被销毁,它就会被摧毁 删除。interpreters.create()Interpreter
属性和方法:
id
(只读)标识 此实例表示的解释器。 从概念上讲,这类似于进程 ID。intInterpreter
__hash__()
返回解释器的 .这是一样的 作为 ID 整数值的哈希值。id
is_running() -> bool
如果解释器当前正在执行代码,则返回 在其模块中。这不包括子线程。True__main__
它仅指是否存在操作系统线程 在解释器模块中运行脚本(代码)。 这基本上意味着是否在某个操作系统线程中运行。在子线程中运行的代码 被忽略。__main__Interpreter.exec()
prepare_main(**kwargs)
在解释器模块中绑定一个或多个对象。__main__
关键字参数名称将用作属性名称。 这些值将绑定为新对象,但完全相同 到原件。仅专门支持传递的对象 允许在口译员之间。请参阅可共享对象。
prepare_main()有助于初始化 在解释器中运行代码之前,将其全局化。
exec(code, /)
在解释器中执行给定的源代码 (在当前操作系统线程中),使用其模块。 它不会返回任何内容。__main__
这基本上等同于切换到这个解释器 在当前操作系统线程中,然后使用此解释器的模块作为调用内置 全球和当地人。exec()__main____dict__
在当前操作系统线程中运行的代码(不同的 interpreter) 有效暂停,直到结束。为避免暂停它,请创建一个新的并调用它 (就像一样)。Interpreter.exec()threading.ThreadInterpreter.exec()Interpreter.call_in_thread()
Interpreter.exec()不会重置解释器的状态,也不会重置 模块,既不前也不后,所以每个 连续呼叫从上一个呼叫中断的地方接听。这可以 对于运行一些代码来初始化解释器很有用 (例如,使用导入),然后再执行一些重复的任务。__main__
如果存在未捕获的异常,它将传播到 调用解释器作为 .完整错误 显示相对于 称为解释器,保留在传播的 . 这包括完整的回溯,以及所有额外的信息,如 语法错误详细信息和链接异常。 如果没有捕获,则显示完整错误 将显示,就像传播的异常一样 在主翻译中长大,未被抓住。拥有 完整回溯在调试时特别有用。ExecutionFailedExecutionFailedExecutionFailed
如果不需要异常传播,则显式 try-except 应在传递给 的代码周围使用 。同样,任何错误处理取决于 关于来自异常的特定信息必须使用显式 try-except 在给定代码周围,因为不会保留该信息。Interpreter.exec()ExecutionFailed
call(callable, /)
在解释器中调用可调用对象。 返回值将被丢弃。如果可调用对象引发异常 然后它被传播为异常, 以与 .ExecutionFailedInterpreter.exec()
目前仅支持普通函数,并且仅支持以下函数 不带参数,也没有单元格变量。免费全局问题已解析 针对目标解释器的模块。__main__
将来,我们可以添加对参数、闭包、 以及种类繁多的可赎回物品,至少部分通过泡菜。 我们也可以考虑不丢弃返回值。 最初的限制已经到位,使我们能够获得基本的 模块的功能更快地提供给用户。
call_in_thread(callable, /) -> threading.Thread
从本质上讲,在新线程中应用。 将丢弃返回值,并且不会传播异常。Interpreter.call()
call_in_thread()大致相当于:
def task(): interp.call(func) t = threading.Thread(target=task) t.start()
口译员之间的沟通
该模块通过特殊的 队列。
有对象,但它们只是代理 实际数据结构:存在的无界FIFO队列 在任何一个口译员之外。这些队列有特殊的住宿条件 用于在解释器之间安全地传递对象数据,而不会违反 解释器隔离。这包括线程安全。interpreters.Queue
与 Python 中的其他队列一样,对于每个“放置”,对象都会被添加到 背面和每个“get”都会从前面弹出下一个。每添加一次 对象将按照它被推送的顺序弹出。
仅专门支持传递的对象 口译员之间可以通过. 请注意,不会发送实际对象,而是发送其 基础数据。但是,弹出的对象仍将是 严格等同于原版。 请参阅可共享对象。interpreters.Queue
该模块定义了以下功能:
create_queue(maxsize=0, *, syncobj=False) -> Queue
创建新队列。如果 maxsize 为零或负数,则 队列是无界的。
“syncobj”用作 和 的默认值。put()put_nowait()
队列对象
interpreters.Queue对象充当基础的代理 模块公开的交叉解释器安全队列。 每个对象都表示队列,并具有相应的 唯一 ID。 任何给定队列都只有一个对象。interpretersQueue
Queue实现了除 和 之外的所有方法,因此它类似于 和 。queue.Queuetask_done()join()asyncio.Queuemultiprocessing.Queue
属性和方法:
id
(只读)标识 相应的交叉解释器队列。 从概念上讲,这类似于文件描述符 用于管道。int
maxsize
(只读)队列中允许的项目数。 零表示“无界”。
__hash__()
返回队列的 .这是一样的 作为 ID 整数值的哈希值。id
empty()
如果队列为空,则返回,否则。TrueFalse
这只是调用时状态的快照。 其他线程或解释器可能会导致此更改。
full()
如果队列中有项目,则返回。Truemaxsize
如果队列初始化为(默认值), 然后永远不会返回.maxsize=0full()True
这只是调用时状态的快照。 其他线程或解释器可能会导致此更改。
qsize()
返回队列中的项数。
这只是调用时状态的快照。 其他线程或解释器可能会导致此更改。
put(obj, timeout=None, *, syncobj=None)
将对象添加到队列中。
如果队列已满,则此块将阻止,直到 有一个空闲的插槽。如果超时为正数 然后它只阻止至少那么多秒,然后提高.否则将永远被阻止。maxsize > 0interpreters.QueueFull
如果 “syncobj” 为 true,则对象必须是可共享的,这意味着对象的数据 是传递的,而不是对象本身。 如果“syncobj”为 false,则支持所有对象。然而 有一些性能损失,所有对象都是副本 (例如通过泡菜)。因此,可变对象永远不会 在口译员之间自动同步。 如果 “syncobj” 为 None(默认值),则队列的默认值为 使用 value。
put_nowait(obj, *, syncobj=None)
喜欢,但有效地立即超时。 因此,如果队列已满,它会立即引发 .put()interpreters.QueueFull
get(timeout=None) -> object
从队列中弹出下一个对象并返回它。阻止时 队列为空。如果提供了积极的超时,并且 对象在那么多秒内未添加到队列中 然后提高.interpreters.QueueEmpty
get_nowait() -> object
喜欢,但不要阻止。如果队列不为空 然后返回下一项。否则,请提高 .get()interpreters.QueueEmpty
可共享对象
Interpreter.prepare_main()仅适用于“可共享”对象。 (可选)也是如此。interpreters.Queue
“可共享”对象是可以从一个解释器传递的对象 到另一个。对象不一定实际直接共享 由口译员提供。但是,即使不是,共享对象 应将其视为直接共享。那是一个 所有可共享对象的强等效性保证。 (见下文。
对于某些类型(内置单例),实际对象是共享的。 对于某些人来说,对象的基础数据实际上是共享的,但每个 Interpreter 有一个不同的对象来包装该数据。对于所有其他 可共享类型,则制作严格的副本或代理,以便 相应的对象继续完全匹配。在以下情况下 基础数据很复杂,但必须复制(例如), 尽可能高效地序列化数据。tuple
内部必须特别支持可共享对象 通过 Python 运行时。但是,对 稍后添加对更多类型的支持。
下面是受支持对象的初始列表:
str
bytes
int
float
bool (True/False)
None
tuple(仅适用于可共享项目)
interpreters.Queue
memoryview(实际共享的基础缓冲区)
请注意,列表中的最后两个 queues 和 是 从技术上讲,数据类型是可变的,而其余的则不是。当任何 解释器共享可变数据,始终存在数据争用的风险。 交叉解释器安全(包括线程安全)是一项基本工作 队列的功能。memoryview
但是,没有任何本地住宿。 用户负责管理线程安全,是否通过 在队列中来回传递以指示安全性的令牌 (请参阅同步),或通过分配子范围独占性 给个别口译员。memoryview
大多数对象将通过队列 (), 作为口译员在彼此之间交流信息。 不太常见的是,在运行代码之前,将通过共享对象来设置解释器。但是,是共享队列的主要方式, 为另一位口译员提供一种手段 进一步沟通。interpreters.Queueprepare_main()prepare_main()
最后,提醒一下:对于一些类型,实际对象是共享的, 而对于其余的,只有基础数据是共享的,无论是 作为副本或通过代理。无论如何,它始终保留 “可共享”对象的强等效保证。
保证一个解释器中的共享对象严格 等价于其他解释器中的相应对象。 换句话说,这两个对象将无法区分 其他。共享对象应被视为原始对象 已经直接共享,无论它是否真的是。 这是一个与平等略有不同和更有力的承诺。
保证对于可变对象(如 和 )尤为重要。更改对象 在一个解释器中,总是会立即反映在每个 其他解释器共享对象。Interpreters.Queuememoryview
同步
在某些情况下,两个解释器应该同步。 这可能涉及共享资源、工作人员管理或保存 顺序一致性。
在线程编程中,典型的同步原语是 互斥锁等类型。该模块公开了几个。 但是,解释器不能共享对象,这意味着他们不能 共享对象。threadingthreading.Lock
该模块不提供任何此类专用 同步基元。相反,对象提供了人们可能需要的一切。interpretersinterpreters.Queue
例如,如果存在需要托管的共享资源 访问,然后可以使用队列来管理它,其中解释器 传递一个对象以指示谁可以使用该资源:
import interpreters from mymodule import load_big_data, check_data numworkers = 10 control = interpreters.create_queue() data = memoryview(load_big_data()) def worker(): interp = interpreters.create() interp.prepare_main(control=control, data=data) interp.exec("""if True: from mymodule import edit_data while True: token = control.get() edit_data(data) control.put(token) """) threads = [threading.Thread(target=worker) for _ in range(numworkers)] for t in threads: t.start() token = 'football' control.put(token) while True: control.get() if not check_data(data): break control.put(token)
异常
type- 原始异常类的表示, 带有 、 和 attrs。__name____module____qualname__
msg - str(exc)原始例外
snapshot- 一个对象 对于原始例外traceback.TracebackException
ExecutionFailed
从未捕获的异常中引发,并在出现异常时引发。 此异常的错误显示包括回溯 未捕获的异常,在正常值之后显示 错误显示,很像 .Interpreter.exec()Interpreter.call()ExceptionGroup
属性:
此异常是 的子类。RuntimeError
QueueEmpty
引发自(或无默认值) 当队列为空时。Queue.get()get_nowait()
此异常是 的子类。queue.Empty
QueueFull
从(超时)或队列已达到其最大大小时引发。Queue.put()put_nowait()
此异常是 的子类。queue.Full
解释器PoolExecutor
除了新模块外,还将有一个新的.每个工作线程执行 在它自己的线程中,有自己的子解释器。沟通可能 仍然通过对象完成, 使用初始值设定项进行设置。interpretersconcurrent.futures.InterpreterPoolExecutorinterpreters.Queue
sys.implementation.supports_isolated_interpreters
不需要 Python 实现来支持子解释器, 尽管大多数主要的都这样做。如果实现确实支持它们 然后将是 设置为 。否则将是.如果功能 不支持,则导入模块将 提出一个 .sys.implementation.supports_isolated_interpretersTrueFalseinterpretersImportError
例子
以下示例演示了多个 口译员可能会有所帮助。
示例 1:
将处理一连串的请求 通过子线程中的 worker。
每个工作线程都有自己的解释器
有一个队列可以将任务发送给工作人员和 另一个返回结果的队列
结果在专用线程中处理
每个工作人员继续前进,直到它收到一个“停止”哨兵(None)
结果处理程序将继续运行,直到所有工作线程都停止
import interpreters from mymodule import iter_requests, handle_result tasks = interpreters.create_queue() results = interpreters.create_queue() numworkers = 20 threads = [] def results_handler(): running = numworkers while running: try: res = results.get(timeout=0.1) except interpreters.QueueEmpty: # No workers have finished a request since last time. pass else: if res is None: # A worker has stopped. running -= 1 else: handle_result(res) empty = object() assert results.get_nowait(empty) is empty threads.append(threading.Thread(target=results_handler)) def worker(): interp = interpreters.create() interp.prepare_main(tasks=tasks, results=results) interp.exec("""if True: from mymodule import handle_request, capture_exception while True: req = tasks.get() if req is None: # Stop! break try: res = handle_request(req) except Exception as exc: res = capture_exception(exc) results.put(res) # Notify the results handler. results.put(None) """) threads.extend(threading.Thread(target=worker) for _ in range(numworkers)) for t in threads: t.start() for req in iter_requests(): tasks.put(req) # Send the "stop" signal. for _ in range(numworkers): tasks.put(None) for t in threads: t.join()
示例 2:
这种情况与上一个情况类似,因为有一群工人 在子线程中。然而,这一次代码是分块一个大数组 的数据,其中每个工作线程一次处理一个块。复制 每个口译员的数据效率将非常低下, 因此,代码利用了直接共享缓冲区的优势。memoryview
所有解释器共享源数组的缓冲区
每个都将其结果写入第二个共享缓冲区
使用队列将任务发送给工作人员
只有一个工作线程会读取源数组中的任何给定索引
只有一个工作线程会写入结果中的任何给定索引 (这就是它确保线程安全的方式)
import interpreters import queue from mymodule import read_large_data_set, use_results numworkers = 3 data, chunksize = read_large_data_set() buf = memoryview(data) numchunks = (len(buf) + 1) / chunksize results = memoryview(b'\0' * numchunks) tasks = interpreters.create_queue() def worker(id): interp = interpreters.create() interp.prepare_main(data=buf, results=results, tasks=tasks) interp.exec("""if True: from mymodule import reduce_chunk while True: req = tasks.get() if res is None: # Stop! break resindex, start, end = req chunk = data[start: end] res = reduce_chunk(chunk) results[resindex] = res """) threads = [threading.Thread(target=worker) for _ in range(numworkers)] for t in threads: t.start() for i in range(numchunks): # Assume there's at least one worker running still. start = i * chunksize end = start + chunksize if end > len(buf): end = len(buf) tasks.put((start, end, i)) # Send the "stop" signal. for _ in range(numworkers): tasks.put(None) for t in threads: t.join() use_results(results)
理由
最小的 API
由于核心开发团队没有真正的经验 用户如何在 Python 代码中使用多个解释器,这 提案有目的地使初始 API 保持精简和最小化,因为 可能。目的是提供一个经过深思熟虑的基础 稍后可以添加更多(更高级)的功能, 视情况而定。
也就是说,拟议的设计吸取了从中吸取的经验教训 社区对子解释器的现有使用,来自现有的 Stdlib 模块,以及来自其他编程语言的模块。它还考虑了以下因素 在 CPython 测试套件中使用子解释器的经验,以及 在并发基准测试中使用它们。
create(), create_queue()
通常,用户调用类型以创建该类型的实例,其中 指向对象的资源被置备。该模块采用不同的方法,用户必须调用以获取新的解释器或新队列。 直接调用仅返回包装器 围绕现有的解释器(同样适用于 )。interpreterscreate()create_queue()interpreters.Interpreter()interpreters.Queue()
这是因为解释器(和队列)是特殊资源。 它们存在于流程中的全局,不由 当前口译员。因此,该模块使创建 解释器(或队列)与创建明显不同的操作 (或 )的实例。interpretersinterpreters.Interpreterinterpreters.Queue
Interpreter.prepare_main() 设置多个变量
prepare_main()可以看作是某种 setter 函数。 它支持一次设置多个名称, 例如,而大多数二传手 一次设置一个项目。主要原因是为了效率。interp.prepare_main(spam=1, eggs=2)
要在解释器中设置一个值, 实现必须首先将操作系统线程切换到标识的 解释器,这涉及一些不可忽略的开销。后 设置它必须切换回来的值。 此外,该机制还存在一些额外的开销 通过它在解释器之间传递对象,可以是 如果一次设置多个值,则聚合减少。__main__.__dict__
因此,支持设置多个 值。prepare_main()
传播异常
子解释器未捕获的异常, 通过 可以(有效地)忽略, 就像一样, 或传播,就像内置一样。 由于是同步操作, 与内置的一样,未捕获的异常也会被传播。Interpreter.exec()threading.Thread()exec()Interpreter.exec()exec()
但是,不会直接提出此类例外情况。那是因为 解释器彼此隔离,不得共享对象, 包括例外情况。这可以通过培养代理人来解决 的异常,无论是摘要、副本还是包装它的代理。 其中任何一个都可以保留回溯,这对于 调试。被抬高的 就是这样的代理人。ExecutionFailed
还有另一个问题需要考虑。如果传播的异常不是 立即被抓住,它会在调用堆栈中冒泡,直到 被抓住(或没有)。如果其他地方的代码可能会捕获它, 确定异常来自子解释器很有帮助 (即“远程”来源),而不是来自当前的解释器。 这就是加薪的原因和原因 它是一个普通的,而不是带有类的副本或代理 与原始异常匹配。例如,从子解释器中捕获的未捕获永远不会在以后的 .相反,必须直接处理。Interpreter.exec()ExecutionFailedExceptionValueErrortry: ... except ValueError: ...ExecutionFailed
相反,从 do 不传播的异常 涉及但直接提出,仿佛起源于 在调用解释器中。这是因为 一种更高级别的方法,它使用 Pickle 来支持不能支持的对象 通常在口译员之间传递。Interpreter.call()ExecutionFailedInterpreter.call()
有限的对象共享
正如 Interpreter Isolation 中所述,只有少量内置 对象可以在解释器之间真正共享。在所有其他情况下 对象只能通过副本或代理间接共享。
可通过队列共享为副本的对象集 (和 ) 是为了 效率。Interpreter.prepare_main()
支持共享所有对象是可能的(通过泡菜) 但不是本提案的一部分。首先,了解这一点很有帮助 在这些情况下,只使用有效的实现。 此外,在这些情况下,通过酸洗来支持可变对象 将违反“共享”对象等效的保证 (并保持这种状态)。
对象与 ID 代理
对于解释器和队列,低级模块使用 通过其对应的 Proxy 对象公开基础状态 进程全局 ID。在这两种情况下,状态同样是流程全局的 并将由多个口译员使用。因此,它们不适合 实现为 ,这实际上只是一个选项 特定于解释器的数据。这就是为什么该模块 而是提供通过 ID 弱关联的对象。PyObjectinterpreters
版权
本文档位于公共领域或 CC0-1.0-通用许可证,以更宽松者为准。
文章声明:以上内容(如有图片或视频在内)除非注明,否则均为腾龙猫勺儿原创文章,转载或复制请以超链接形式并注明出处。
本文作者:猫勺本文链接:https://www.jo6.cn/post/75.html