PEP 334 – 通过 SuspendIteration 实现简单协程
- 作者:
- Clark C. Evans <cce at clarkevans.com>
- 状态:
- 已撤回
- 类型:
- 标准轨迹
- 创建:
- 2004年8月26日
- Python 版本:
- 3.0
- 历史记录:
摘要
像 Twisted [1] 和 Peak [2] 这样的异步应用程序框架,基于通过事件队列或延迟执行实现的协作式多任务处理。虽然这种应用程序开发方法不涉及线程,从而避免了一整类问题 [3],但它也带来了不同类型的编程挑战。当 I/O 操作发生阻塞时,用户请求必须挂起,以便其他请求可以继续进行。协程的概念 [4] 有望帮助应用程序开发者应对这种状态管理难题。
本 PEP 提出了一种基于对 迭代器协议 的扩展的有限协程方法。目前,迭代器可以通过引发 StopIteration 异常来指示它已完成值的生成。本提案为此协议添加了另一个异常,即 SuspendIteration,它表示给定的迭代器可能还有更多值要生成,但目前无法生成。
基本原理
目前有两种将协程引入 Python 的方法。Christian Tismer 的 Stackless [6] 涉及通过修改“C”栈来彻底重构 Python 的执行模型。虽然这种方法有效,但其操作难以描述且难以保持可移植性。另一种相关的方法是将 Python 代码编译成 Parrot [7],这是一种基于寄存器的虚拟机,它具有协程。不幸的是,这两种解决方案都不能与 IronPython (CLR) 或 Jython (JavaVM) 兼容。
人们认为,一种更有限的方法,基于迭代器,可以为应用程序程序员提供协程功能,并且仍然可以在各种运行时环境中移植。
- 迭代器将其状态保存在本地变量中,这些变量不在“C”栈上。迭代器可以被视为类,其状态存储在成员变量中,这些变量在对 next() 方法的每次调用之间都是持久的。
- 虽然未捕获的异常可能会终止函数的执行,但未捕获的异常不必使迭代器失效。提议的异常 SuspendIteration 利用了此特性。换句话说,仅仅因为一次对 next() 的调用导致了异常,并不一定意味着迭代器本身不再能够生成值。
此新异常会影响四个方面
- PEP 255 的简单生成器机制可以扩展为安全地“捕获”此 SuspendIteration 异常,存储其当前状态,并将异常传递给调用者。
- 标准库中的各种迭代器过滤器 [9],例如 itertools.izip,应能够感知此异常,以便能够透明地传播 SuspendIteration。
- 从 I/O 操作(例如文件或套接字读取器)生成的迭代器可以修改为具有非阻塞变体。如果请求的操作会导致阻塞,则此选项将引发 SuspendIteration 的子类。
- asyncore 库可以更新为提供一个基本的“运行器”,该运行器从迭代器中提取数据;如果捕获到 SuspendIteration 异常,则它将转到其运行列表中的下一个迭代器 [10]。Twisted 等外部框架将提供替代实现,也许基于 FreeBSD 的 kqueue 或 Linux 的 epoll。
虽然这些看起来像是巨大的变化,但与延续提供的实用程序相比,这是一项非常少量的劳动。
语义
本节将高层次地解释引入此新的 SuspendIteration 异常的行为方式。
简单迭代器
迭代器的当前功能最好通过一个简单的示例来说明,该示例生成两个值“one”和“two”。
class States:
def __iter__(self):
self._next = self.state_one
return self
def next(self):
return self._next()
def state_one(self):
self._next = self.state_two
return "one"
def state_two(self):
self._next = self.state_stop
return "two"
def state_stop(self):
raise StopIteration
print list(States())
当然,可以使用以下生成器创建等效的迭代:
def States():
yield 'one'
yield 'two'
print list(States())
引入 SuspendIteration
假设在生成“one”和“two”之间,上面的生成器可能会阻塞套接字读取。在这种情况下,我们希望引发 SuspendIteration 来表示迭代器尚未完成生成,但目前无法提供值。
from random import randint
from time import sleep
class SuspendIteration(Exception):
pass
class NonBlockingResource:
"""Randomly unable to produce the second value"""
def __iter__(self):
self._next = self.state_one
return self
def next(self):
return self._next()
def state_one(self):
self._next = self.state_suspend
return "one"
def state_suspend(self):
rand = randint(1,10)
if 2 == rand:
self._next = self.state_two
return self.state_two()
raise SuspendIteration()
def state_two(self):
self._next = self.state_stop
return "two"
def state_stop(self):
raise StopIteration
def sleeplist(iterator, timeout = .1):
"""
Do other things (e.g. sleep) while resource is
unable to provide the next value
"""
it = iter(iterator)
retval = []
while True:
try:
retval.append(it.next())
except SuspendIteration:
sleep(timeout)
continue
except StopIteration:
break
return retval
print sleeplist(NonBlockingResource())
在实际情况中,NonBlockingResource 将是一个文件迭代器、套接字句柄或其他基于 I/O 的生成器。sleeplist 将是一个异步反应器,例如 asyncore 或 Twisted 中的反应器。当然,非阻塞资源也可以编写成生成器
def NonBlockingResource():
yield "one"
while True:
rand = randint(1,10)
if 2 == rand:
break
raise SuspendIteration()
yield "two"
无需添加关键字“suspend”,因为大多数实际内容生成器都不会位于应用程序代码中,它们将位于低级基于 I/O 的操作中。由于大多数程序员不需要接触 SuspendIteration() 机制,因此不需要关键字。
应用迭代器
前面的示例相当牵强,一个更“现实”的示例是一个网页生成器,它生成 HTML 内容并从数据库中提取数据。请注意,这不是“生产者”或“消费者”的示例,而是过滤器的示例。
def ListAlbums(cursor):
cursor.execute("SELECT title, artist FROM album")
yield '<html><body><table><tr><td>Title</td><td>Artist</td></tr>'
for (title, artist) in cursor:
yield '<tr><td>%s</td><td>%s</td></tr>' % (title, artist)
yield '</table></body></html>'
当然,问题在于数据库可能在返回任何行之前阻塞一段时间,并且在执行期间,行可能会一次返回 10 或 100 个块。理想情况下,如果数据库阻塞以获取下一组行,则可以服务于另一个用户连接。请注意上述代码中完全没有 SuspendIterator。如果操作正确,应用程序开发人员将能够专注于功能而不是并发问题。
由上述生成器创建的迭代器应该执行必要的魔术来维护状态,但将异常传递给更低级别的异步框架。以下是如何将相应迭代器编码为类的示例
class ListAlbums:
def __init__(self, cursor):
self.cursor = cursor
def __iter__(self):
self.cursor.execute("SELECT title, artist FROM album")
self._iter = iter(self._cursor)
self._next = self.state_head
return self
def next(self):
return self._next()
def state_head(self):
self._next = self.state_cursor
return "<html><body><table><tr><td>\
Title</td><td>Artist</td></tr>"
def state_tail(self):
self._next = self.state_stop
return "</table></body></html>"
def state_cursor(self):
try:
(title,artist) = self._iter.next()
return '<tr><td>%s</td><td>%s</td></tr>' % (title, artist)
except StopIteration:
self._next = self.state_tail
return self.next()
except SuspendIteration:
# just pass-through
raise
def state_stop(self):
raise StopIteration
复杂因素
虽然上面的示例很简单,但如果中间生成器“压缩”值,即为它生成的每个值提取两个或多个值,则情况会稍微复杂一些。例如,
def pair(iterLeft,iterRight):
rhs = iter(iterRight)
lhs = iter(iterLeft)
while True:
yield (rhs.next(), lhs.next())
在这种情况下,相应的迭代器行为必须更加微妙,才能处理右侧或左侧迭代器引发 SuspendIteration 的情况。这似乎是分解生成器以识别产生上下文可能发生 SuspendIterator 异常的中间状态的问题。
class pair:
def __init__(self, iterLeft, iterRight):
self.iterLeft = iterLeft
self.iterRight = iterRight
def __iter__(self):
self.rhs = iter(iterRight)
self.lhs = iter(iterLeft)
self._temp_rhs = None
self._temp_lhs = None
self._next = self.state_rhs
return self
def next(self):
return self._next()
def state_rhs(self):
self._temp_rhs = self.rhs.next()
self._next = self.state_lhs
return self.next()
def state_lhs(self):
self._temp_lhs = self.lhs.next()
self._next = self.state_pair
return self.next()
def state_pair(self):
self._next = self.state_rhs
return (self._temp_rhs, self._temp_lhs)
本提案假设对于现有的生成器,可以使用这种基于类的编写相应迭代器。挑战似乎在于识别生成器中可能发生挂起的不同状态。
资源清理
当前的生成器机制与异常之间存在奇怪的交互,其中在 try/finally 块中不允许使用“yield”语句。SuspendIterator 异常带来了另一个类似的问题。此问题的影响尚不清楚。但是,可能将生成器重写为状态机,如上一节所述,可以解决此问题,使情况不比以前更糟,甚至可能消除 yield/finally 情况。这方面需要进一步研究。
API 和限制
本提案仅涵盖“挂起”迭代器链,当然不涵盖挂起通用函数、方法或“C”扩展函数。虽然可能无法直接支持在“C”代码中创建生成器,但肯定可以创建符合 SuspendIterator 语义的本机“C”迭代器。
底层实现
PEP 的作者还不熟悉 Python 执行模型,因此无法在此方面发表评论。
参考文献
版权
本文档已进入公有领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0334.rst
上次修改时间:2023年9月9日 17:39:29 GMT