PEP 734 – Stdlib 中的多个解释器

猫勺猫勺 03-25 1.49 K 阅读

注意

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()

  1. 创建新的解释器状态

  2. 创建新的线程状态

  3. 将线程状态设置为当前 (解释器初始化需要当前状态)

  4. 使用该线程状态初始化解释器状态

  5. 返回线程状态(仍为当前状态)

请注意,返回的线程状态可能会立即被丢弃。 不要求解释器具有任何线程状态, 除非口译员是要实际使用的。 此时,它必须在当前操作系统线程中处于活动状态。

要使现有解释器在当前操作系统线程中处于活动状态, 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-通用许可证,以更宽松者为准。

The End 微信扫一扫

文章声明:以上内容(如有图片或视频在内)除非注明,否则均为腾龙猫勺儿原创文章,转载或复制请以超链接形式并注明出处。

本文作者:猫勺本文链接:https://www.jo6.cn/post/75.html

上一篇 下一篇

相关阅读

取消
微信二维码
微信二维码
支付宝二维码