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](一个基于寄存器的虚拟机),它具有协程。不幸的是,这些解决方案 neither 无法在 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。
虽然这些变化可能看起来很剧烈,但与 Continuation 提供的实用性相比,所需工作量非常小。
语义
本节将从高层次解释此新的 SuspendIteration 异常的引入将如何表现。
简单的迭代器
迭代器当前的 P functionality 最好通过一个产生两个值“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 的情况差,甚至可能消除 yield/finally 的情况。这方面需要更多的调查。
API 和限制
本提案仅涵盖“暂停”一系列迭代器,当然也不涵盖暂停通用函数、方法或“C”扩展函数。虽然不能直接支持用“C”代码创建生成器,但符合 SuspendIterator 语义的原生“C”迭代器当然是可能的。
底层实现
PEP 的作者目前还不熟悉 Python 的执行模型,无法对此领域发表评论。
参考资料
版权
本文档已置于公共领域。
Source: https://github.com/python/peps/blob/main/peps/pep-0334.rst
Last modified: 2025-02-01 08:59:27 GMT