2004-09-07 20:11:18 -04:00
|
|
|
|
PEP: 334
|
|
|
|
|
Title: Simple Coroutines via SuspendIteration
|
|
|
|
|
Version: $Revision$
|
|
|
|
|
Last-Modified: $Date$
|
2007-06-27 20:08:00 -04:00
|
|
|
|
Author: Clark C. Evans <cce@clarkevans.com>
|
2006-05-11 03:50:27 -04:00
|
|
|
|
Status: Withdrawn
|
2004-09-07 20:11:18 -04:00
|
|
|
|
Type: Standards Track
|
|
|
|
|
Content-Type: text/x-rst
|
|
|
|
|
Created: 26-Aug-2004
|
2007-06-19 00:20:07 -04:00
|
|
|
|
Python-Version: 3.0
|
2004-09-07 20:11:18 -04:00
|
|
|
|
Post-History:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Abstract
|
|
|
|
|
========
|
|
|
|
|
|
|
|
|
|
Asynchronous application frameworks such as Twisted [1]_ and Peak
|
|
|
|
|
[2]_, are based on a cooperative multitasking via event queues or
|
|
|
|
|
deferred execution. While this approach to application development
|
|
|
|
|
does not involve threads and thus avoids a whole class of problems
|
|
|
|
|
[3]_, it creates a different sort of programming challenge. When an
|
|
|
|
|
I/O operation would block, a user request must suspend so that other
|
|
|
|
|
requests can proceed. The concept of a coroutine [4]_ promises to
|
|
|
|
|
help the application developer grapple with this state management
|
|
|
|
|
difficulty.
|
|
|
|
|
|
|
|
|
|
This PEP proposes a limited approach to coroutines based on an
|
|
|
|
|
extension to the iterator protocol [5]_. Currently, an iterator may
|
|
|
|
|
raise a StopIteration exception to indicate that it is done producing
|
|
|
|
|
values. This proposal adds another exception to this protocol,
|
|
|
|
|
SuspendIteration, which indicates that the given iterator may have
|
|
|
|
|
more values to produce, but is unable to do so at this time.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Rationale
|
|
|
|
|
=========
|
|
|
|
|
|
|
|
|
|
There are two current approaches to bringing co-routines to Python.
|
|
|
|
|
Christian Tismer's Stackless [6]_ involves a ground-up restructuring
|
|
|
|
|
of Python's execution model by hacking the 'C' stack. While this
|
|
|
|
|
approach works, its operation is hard to describe and keep portable. A
|
|
|
|
|
related approach is to compile Python code to Parrot [7]_, a
|
|
|
|
|
register-based virtual machine, which has coroutines. Unfortunately,
|
|
|
|
|
neither of these solutions is portable with IronPython (CLR) or Jython
|
|
|
|
|
(JavaVM).
|
|
|
|
|
|
|
|
|
|
It is thought that a more limited approach, based on iterators, could
|
|
|
|
|
provide a coroutine facility to application programmers and still be
|
|
|
|
|
portable across runtimes.
|
|
|
|
|
|
|
|
|
|
* Iterators keep their state in local variables that are not on the
|
|
|
|
|
"C" stack. Iterators can be viewed as classes, with state stored in
|
|
|
|
|
member variables that are persistent across calls to its next()
|
|
|
|
|
method.
|
|
|
|
|
|
|
|
|
|
* While an uncaught exception may terminate a function's execution, an
|
|
|
|
|
uncaught exception need not invalidate an iterator. The proposed
|
|
|
|
|
exception, SuspendIteration, uses this feature. In other words,
|
|
|
|
|
just because one call to next() results in an exception does not
|
|
|
|
|
necessarily need to imply that the iterator itself is no longer
|
|
|
|
|
capable of producing values.
|
|
|
|
|
|
|
|
|
|
There are four places where this new exception impacts:
|
|
|
|
|
|
|
|
|
|
* The simple generator [8]_ mechanism could be extended to safely
|
|
|
|
|
'catch' this SuspendIteration exception, stuff away its current
|
|
|
|
|
state, and pass the exception on to the caller.
|
|
|
|
|
|
|
|
|
|
* Various iterator filters [9]_ in the standard library, such as
|
|
|
|
|
itertools.izip should be made aware of this exception so that it can
|
|
|
|
|
transparently propagate SuspendIteration.
|
|
|
|
|
|
|
|
|
|
* Iterators generated from I/O operations, such as a file or socket
|
|
|
|
|
reader, could be modified to have a non-blocking variety. This
|
|
|
|
|
option would raise a subclass of SuspendIteration if the requested
|
|
|
|
|
operation would block.
|
|
|
|
|
|
|
|
|
|
* The asyncore library could be updated to provide a basic 'runner'
|
|
|
|
|
that pulls from an iterator; if the SuspendIteration exception is
|
|
|
|
|
caught, then it moves on to the next iterator in its runlist [10]_.
|
|
|
|
|
External frameworks like Twisted would provide alternative
|
|
|
|
|
implementations, perhaps based on FreeBSD's kqueue or Linux's epoll.
|
|
|
|
|
|
|
|
|
|
While these may seem dramatic changes, it is a very small amount of
|
|
|
|
|
work compared with the utility provided by continuations.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Semantics
|
|
|
|
|
=========
|
|
|
|
|
|
|
|
|
|
This section will explain, at a high level, how the introduction of
|
|
|
|
|
this new SuspendIteration exception would behave.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Simple Iterators
|
|
|
|
|
----------------
|
|
|
|
|
|
|
|
|
|
The current functionality of iterators is best seen with a simple
|
|
|
|
|
example which produces two values 'one' and '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())
|
|
|
|
|
|
|
|
|
|
An equivalent iteration could, of course, be created by the
|
|
|
|
|
following generator::
|
|
|
|
|
|
|
|
|
|
def States():
|
|
|
|
|
yield 'one'
|
|
|
|
|
yield 'two'
|
|
|
|
|
|
|
|
|
|
print list(States())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Introducing SuspendIteration
|
|
|
|
|
----------------------------
|
|
|
|
|
|
|
|
|
|
Suppose that between producing 'one' and 'two', the generator above
|
|
|
|
|
could block on a socket read. In this case, we would want to raise
|
|
|
|
|
SuspendIteration to signal that the iterator is not done producing,
|
|
|
|
|
but is unable to provide a value at the current moment. ::
|
|
|
|
|
|
|
|
|
|
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())
|
|
|
|
|
|
|
|
|
|
In a real-world situation, the NonBlockingResource would be a file
|
|
|
|
|
iterator, socket handle, or other I/O based producer. The sleeplist
|
|
|
|
|
would instead be an async reactor, such as those found in asyncore or
|
|
|
|
|
Twisted. The non-blocking resource could, of course, be written as a
|
|
|
|
|
generator::
|
|
|
|
|
|
|
|
|
|
def NonBlockingResource():
|
|
|
|
|
yield "one"
|
|
|
|
|
while True:
|
|
|
|
|
rand = randint(1,10)
|
|
|
|
|
if 2 == rand:
|
|
|
|
|
break
|
|
|
|
|
raise SuspendIteration()
|
|
|
|
|
yield "two"
|
|
|
|
|
|
|
|
|
|
It is not necessary to add a keyword, 'suspend', since most real
|
|
|
|
|
content generators will not be in application code, they will be in
|
|
|
|
|
low-level I/O based operations. Since most programmers need not be
|
|
|
|
|
exposed to the SuspendIteration() mechanism, a keyword is not needed.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Application Iterators
|
|
|
|
|
---------------------
|
|
|
|
|
|
|
|
|
|
The previous example is rather contrived, a more 'real-world' example
|
|
|
|
|
would be a web page generator which yields HTML content, and pulls
|
|
|
|
|
from a database. Note that this is an example of neither the
|
|
|
|
|
'producer' nor the 'consumer', but rather of a filter. ::
|
|
|
|
|
|
|
|
|
|
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>'
|
|
|
|
|
|
|
|
|
|
The problem, of course, is that the database may block for some time
|
|
|
|
|
before any rows are returned, and that during execution, rows may be
|
|
|
|
|
returned in blocks of 10 or 100 at a time. Ideally, if the database
|
|
|
|
|
blocks for the next set of rows, another user connection could be
|
|
|
|
|
serviced. Note the complete absence of SuspendIterator in the above
|
|
|
|
|
code. If done correctly, application developers would be able to
|
|
|
|
|
focus on functionality rather than concurrency issues.
|
|
|
|
|
|
|
|
|
|
The iterator created by the above generator should do the magic
|
|
|
|
|
necessary to maintain state, yet pass the exception through to a
|
|
|
|
|
lower-level async framework. Here is an example of what the
|
|
|
|
|
corresponding iterator would look like if coded up as a class::
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Complicating Factors
|
|
|
|
|
--------------------
|
|
|
|
|
|
|
|
|
|
While the above example is straight-forward, things are a bit more
|
|
|
|
|
complicated if the intermediate generator 'condenses' values, that is,
|
|
|
|
|
it pulls in two or more values for each value it produces. For
|
|
|
|
|
example, ::
|
|
|
|
|
|
|
|
|
|
def pair(iterLeft,iterRight):
|
|
|
|
|
rhs = iter(iterRight)
|
|
|
|
|
lhs = iter(iterLeft)
|
|
|
|
|
while True:
|
|
|
|
|
yield (rhs.next(), lhs.next())
|
|
|
|
|
|
|
|
|
|
In this case, the corresponding iterator behavior has to be a bit more
|
|
|
|
|
subtle to handle the case of either the right or left iterator raising
|
|
|
|
|
SuspendIteration. It seems to be a matter of decomposing the
|
|
|
|
|
generator to recognize intermediate states where a SuspendIterator
|
|
|
|
|
exception from the producing context could happen. ::
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
This proposal assumes that a corresponding iterator written using
|
|
|
|
|
this class-based method is possible for existing generators. The
|
|
|
|
|
challenge seems to be the identification of distinct states within
|
|
|
|
|
the generator where suspension could occur.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Resource Cleanup
|
|
|
|
|
----------------
|
|
|
|
|
|
|
|
|
|
The current generator mechanism has a strange interaction with
|
|
|
|
|
exceptions where a 'yield' statement is not allowed within a
|
|
|
|
|
try/finally block. The SuspendIterator exception provides another
|
|
|
|
|
similar issue. The impacts of this issue are not clear. However it
|
|
|
|
|
may be that re-writing the generator into a state machine, as the
|
|
|
|
|
previous section did, could resolve this issue allowing for the
|
|
|
|
|
situation to be no-worse than, and perhaps even removing the
|
|
|
|
|
yield/finally situation. More investigation is needed in this area.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
API and Limitations
|
|
|
|
|
-------------------
|
|
|
|
|
|
|
|
|
|
This proposal only covers 'suspending' a chain of iterators, and does
|
|
|
|
|
not cover (of course) suspending general functions, methods, or "C"
|
|
|
|
|
extension function. While there could be no direct support for
|
|
|
|
|
creating generators in "C" code, native "C" iterators which comply
|
|
|
|
|
with the SuspendIterator semantics are certainly possible.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Low-Level Implementation
|
|
|
|
|
========================
|
|
|
|
|
|
|
|
|
|
The author of the PEP is not yet familiar with the Python execution
|
|
|
|
|
model to comment in this area.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
References
|
|
|
|
|
==========
|
|
|
|
|
|
|
|
|
|
.. [1] Twisted
|
|
|
|
|
(http://twistedmatrix.com)
|
|
|
|
|
|
|
|
|
|
.. [2] Peak
|
|
|
|
|
(http://peak.telecommunity.com)
|
|
|
|
|
|
|
|
|
|
.. [3] C10K
|
|
|
|
|
(http://www.kegel.com/c10k.html)
|
|
|
|
|
|
|
|
|
|
.. [4] Coroutines
|
|
|
|
|
(http://c2.com/cgi/wiki?CallWithCurrentContinuation)
|
|
|
|
|
|
|
|
|
|
.. [5] PEP 234, Iterators
|
|
|
|
|
(http://www.python.org/peps/pep-0234.html)
|
|
|
|
|
|
|
|
|
|
.. [6] Stackless Python
|
|
|
|
|
(http://stackless.com)
|
|
|
|
|
|
|
|
|
|
.. [7] Parrot /w coroutines
|
|
|
|
|
(http://www.sidhe.org/~dan/blog/archives/000178.html)
|
|
|
|
|
|
|
|
|
|
.. [8] PEP 255, Simple Generators
|
|
|
|
|
(http://www.python.org/peps/pep-0255.html)
|
|
|
|
|
|
|
|
|
|
.. [9] itertools - Functions creating iterators
|
|
|
|
|
(http://docs.python.org/lib/module-itertools.html)
|
|
|
|
|
|
|
|
|
|
.. [10] Microthreads in Python, David Mertz
|
|
|
|
|
(http://www-106.ibm.com/developerworks/linux/library/l-pythrd.html)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Copyright
|
|
|
|
|
=========
|
|
|
|
|
|
|
|
|
|
This document has been placed in the public domain.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
..
|
|
|
|
|
Local Variables:
|
|
|
|
|
mode: indented-text
|
|
|
|
|
indent-tabs-mode: nil
|
|
|
|
|
sentence-end-double-space: t
|
|
|
|
|
fill-column: 70
|
|
|
|
|
End:
|