PEP 219 – 无栈 Python
- 作者:
- Gordon McMillan <gmcm at hypernet.com>
- 状态:
- 推迟
- 类型:
- 标准跟踪
- 创建日期:
- 2000年8月14日
- Python 版本:
- 2.1
- 发布历史:
引言
本 PEP 讨论了对 Python 核心进行更改以有效支持生成器、微线程和协程的必要性。它与 PEP 220 相关,后者描述了如何扩展 Python 以支持这些功能。本 PEP 的重点严格在于实现这些扩展所需的更改。
虽然这些 PEP 基于 Christian Tismer 的无栈 [1] 实现,但它们不将无栈视为参考实现。无栈(带有一个扩展模块)实现了续体,通过续体可以实现协程、微线程(Will Ware 已经完成 [2])和生成器。但在一年多的时间里,没有人发现续体的其他有效用途,因此似乎没有支持它们的需求。
然而,无栈对续体的支持是实现中相对较小的一部分,因此可以将其视为“一个”参考实现(而不是“唯一的”参考实现)。
背景
生成器和协程已在多种语言中以多种方式实现。事实上,Tim Peters 已经使用线程(并且 Java 存在基于线程的协程实现)纯 Python 实现了生成器 [3] 和协程 [4]。然而,基于线程实现的巨大开销严重限制了这种方法的实用性。
微线程(又称“绿色”或“用户”线程)和协程涉及控制转移,这在基于单个堆栈的语言实现中很难适应。(生成器可以在单个堆栈上完成,但它们也可以被视为协程的一个非常简单的案例。)
真实的线程为每个控制线程分配一个全尺寸的堆栈,这是开销的主要来源。然而,协程和微线程可以在 Python 中以几乎没有开销的方式实现。因此,本 PEP 提供了一种使 Python 能够实际管理数千个独立“线程”活动(与当今大约数十个独立线程活动的限制相比)的方法。
本 PEP 的另一个理由(在 PEP 220 中探讨)是协程和生成器通常允许比当今 Python 更直接地表达算法。
讨论
首先要注意的是,Python 虽然在堆栈上混合了解释器数据(正常的 C 堆栈使用)和 Python 数据(被解释程序的 state),但两者在逻辑上是独立的。它们只是恰好使用相同的堆栈。
一个真实的线程会获得一个接近进程大小的堆栈,因为实现无法知道该线程需要多少堆栈空间。单个帧所需的堆栈空间可能合理,但堆栈切换是一个神秘且不可移植的过程,不受 C 的支持。
然而,一旦 Python 不再将 Python 数据放在 C 堆栈上,堆栈切换就变得容易了。
本 PEP 的基本方法基于这两个思想。首先,将 C 的堆栈使用与 Python 的堆栈使用分开。其次,为每个帧分配足够的堆栈空间以处理该帧的执行。
在正常使用中,无栈 Python 具有正常的堆栈结构,只是它被分解成块。但在存在协程/微线程扩展的情况下,相同的机制支持具有树状结构的堆栈。也就是说,扩展可以支持在正常的“调用/返回”路径之外的帧之间进行控制转移。
问题
这种方法的主要困难是 C 调用 Python。问题在于 C 堆栈现在持有字节码解释器的嵌套执行。在这种情况下,不允许协程/微线程扩展将控制权转移到不同字节码解释器调用中的帧。如果一个帧完成并从错误的解释器返回到 C,C 堆栈可能会被破坏。
理想的解决方案是创建一个机制,使字节码解释器的嵌套执行永远不需要。简单的解决方案是让协程/微线程扩展识别这种情况并拒绝允许在当前调用之外进行转移。
我们可以将涉及 C 调用 Python 的代码分为两类:Python 的实现和 C 扩展。我们希望能够提供一个折衷方案:Python 的内部使用(以及愿意付出努力的 C 扩展编写者)将不再使用解释器的嵌套调用。不付出努力的扩展仍然是安全的,但不能很好地与协程/微线程配合。
通常,当递归调用转换为循环时,需要一些额外的簿记。循环将需要保留自己的参数和结果“堆栈”,因为真实的堆栈现在只能容纳最近的。代码会更冗长,因为何时完成不再那么明显。虽然无栈不是这样实现的,但它必须处理相同的问题。
在普通 Python 中,PyEval_EvalCode 用于构建和执行帧。无栈 Python 引入了 FrameDispatcher 的概念。像 PyEval_EvalCode 一样,它执行一个帧。但解释器可能会向 FrameDispatcher 发出信号,表明已交换进一个新帧,并且应该执行新帧。当一个帧完成时,FrameDispatcher 会沿着回溯指针恢复“调用”帧。
因此,无栈将递归转换为循环,但管理帧的不是 FrameDispatcher。这是由解释器(或知道自己在做什么的扩展)完成的。
一般的想法是,当 C 代码需要执行 Python 代码时,它会为 Python 代码创建一个帧,将其回溯指针设置为当前帧。然后它交换入帧,向 FrameDispatcher 发出信号并退出。C 堆栈现在是干净的 - Python 代码可以将控制权转移到任何其他帧(如果扩展为其提供了这样做的手段)。
在普通情况下,这种魔力可以对程序员(甚至在大多数情况下,对 Python 内部程序员)隐藏。然而,许多情况会带来另一个层面的困难。
内置函数 map 对这种方法造成了两个障碍。它不能简单地构造一个帧然后退出,不仅仅因为涉及循环,而且循环的每次通过都需要一些“后”处理。为了与其他组件良好配合,无栈为 map 本身构造了一个帧对象。
解释器的大多数递归都没有这么复杂,但相当频繁地需要一些“后”操作。无栈不修复这些情况,因为需要大量的代码更改。相反,无栈禁止从嵌套解释器中进行转移。虽然不理想(有时令人费解),但这个限制几乎没有瘫痪性。
优点
对于普通 Python,这种方法的优点是 C 堆栈使用变得更小且更可预测。Python 代码中无限制的递归成为内存错误,而不是堆栈错误(因此,在非 Cupertino 操作系统中,是可以恢复的)。当然,代价是由于将字节码解释器循环的递归转换为更高阶循环(以及随之而来的簿记)而增加的复杂性。
最大的优势来自于认识到 Python 堆栈实际上是一棵树,帧调度器可以在树的叶节点之间自由地转移控制权,从而允许微线程和协程等功能。
参考资料
来源:https://github.com/python/peps/blob/main/peps/pep-0219.rst