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])和生成器。但在一年多的时间里,没有人发现延续的其他任何生产性用途,因此似乎没有对其支持的需求。
然而,无栈对延续的支持是实现中相对较小的一部分,因此人们可能会将其视为“一个”参考实现(而不是“the”参考实现)。
背景
生成器和协程已在多种语言中以多种方式实现。实际上,Tim Peters已经使用线程对生成器[3]和协程[4]进行了纯Python实现(并且Java存在基于线程的协程实现)。但是,基于线程的实现的巨大开销严重限制了这种方法的实用性。
微线程(又称“绿色”或“用户”线程)和协程涉及控制转移,这在基于单个栈的语言实现中难以适应。(生成器可以在单个栈上完成,但它们也可以被视为协程的一种非常简单的情况。)
真正的线程为每个控制线程分配一个完整大小的栈,这是开销的主要来源。但是,可以在Python中以几乎不产生开销的方式实现协程和微线程。因此,本PEP提供了一种方法,使Python能够实际管理数千个单独的活动“线程”(相对于当今可能只有几十个单独的活动线程的限制)。
本PEP的另一个理由(在PEP 220中探讨)是,协程和生成器通常允许比当今的Python中可能的方式更直接地表达算法。
讨论
首先要注意的是,虽然Python将解释器数据(正常的C栈使用)与Python数据(解释程序的状态)混合在栈上,但两者在逻辑上是分开的。它们碰巧使用了相同的栈。
一个真正的线程获得类似于进程大小的栈,因为实现无法知道线程将需要多少栈空间。单个帧所需的栈空间可能相当合理,但栈切换是一个神秘且不可移植的过程,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