Add section on coroutines and the scheduler.

This commit is contained in:
Guido van Rossum 2012-12-13 17:47:40 -08:00
parent 000070388a
commit 3e3fce8960
1 changed files with 122 additions and 13 deletions

View File

@ -16,7 +16,7 @@ This is a proposal for asynchronous I/O in Python 3, starting with
Python 3.3. Consider this the concrete proposal that is missing from
PEP 3153. The proposal includes a pluggable event loop API, transport
and protocol abstractions similar to those in Twisted, and a
higher-level scheduler based on yield-from (PEP 380). A reference
higher-level scheduler based on ``yield from`` (PEP 380). A reference
implementation is in the works under the code name tulip.
@ -50,7 +50,7 @@ Thus, two separate APIs are defined:
An event loop implementation may provide additional methods and
guarantees.
The event loop interface does not depend on yield-from. Rather, it
The event loop interface does not depend on ``yield from``. Rather, it
uses a combination of callbacks, additional interfaces (transports and
protocols), and Futures. The latter are similar to those defined in
PEP 3148, but have a different implementation and are not tied to
@ -59,7 +59,7 @@ expected to use callbacks.
For users (like myself) who don't like using callbacks, a scheduler is
provided for writing asynchronous I/O code as coroutines using the PEP
380 yield-from expressions. The scheduler is not pluggable;
380 ``yield from`` expressions. The scheduler is not pluggable;
pluggability occurs at the event loop level, and the scheduler should
work with any conforming event loop implementation.
@ -406,9 +406,10 @@ The internal methods defined in PEP 3148 are not supported.
A ``tulip.Future`` object is not acceptable to the ``wait()`` and
``as_completed()`` functions in the ``concurrent.futures`` package.
A ``tulip.Future`` object is acceptable to a yield-from expression
when used in a coroutine. See the section "Coroutines and the
Scheduler" below.
A ``tulip.Future`` object is acceptable to a ``yield from`` expression
when used in a coroutine. This is implemented through the
``__iter__()`` interface on the Future. See the section "Coroutines
and the Scheduler" below.
Transports
----------
@ -551,11 +552,6 @@ TBD: How do we detect a half-close (``write_eof()`` in our parlance)
initiated by the other end? Does this call connection_lost()? Is the
protocol then allowed to write more? (I think it should be!)
Coroutines and the Scheduler
----------------------------
TBD.
Callback Style
--------------
@ -583,6 +579,106 @@ and how to override the choice. Probably belongs in the event loop
policy.)
Coroutines and the Scheduler
============================
This is a separate toplevel section because its status is different
from the event loop interface. Usage of coroutines is optional, and
it is perfectly fine to write code using callbacks only. On the other
hand, there is only one implementation of the scheduler/coroutine API,
and if you're using coroutines, that's the one you're using.
A coroutine is a generator that follows certain conventions. For
documentation purposes, all coroutines should be decorated with
``@tulip.coroutine``, but this cannot be strictly enforced.
Coroutines use the ``yield from`` syntax introduced in PEP 380,
instead of the original ``yield`` syntax.
Unfortunately, the word "coroutine", like the word "generator", is
used for two different (though related) concepts:
- The function that defines a coroutine (a function definition
decorated with ``tulip.coroutine``). If disambiguation is needed,
we call this a *coroutine function*.
- The object obtained by calling a coroutine function. This object
represents a computation or an I/O operation (usually a combination)
that will complete eventually. For disambiguation we call it a
*coroutine object*.
Things a coroutine can do:
- ``result = yield from future`` -- suspends the coroutine until the
future is done, then returns the future's result, or raises its
exception, which will be propagated.
- ``result = yield from coroutine`` -- wait for another coroutine to
produce a result (or raise an exception, which will be propagated).
The ``coroutine`` expression must be a *call* to another coroutine.
- ``results = yield from tulip.par(futures_and_coroutines)`` -- Wait
for a list of futures and/or coroutines to complete and return a
list of their results. If one of the futures or coroutines raises
an exception, that exception is propagated, after attempting to
cancel all other futures and coroutines in the list.
- ``return result`` -- produce a result to the coroutine that is
waiting for this one using ``yield from``.
- ``raise exception`` -- raise an exception in the coroutine that is
waiting for this one using ``yield from``.
Calling a coroutine does not start its code running -- it is just a
generator, and the coroutine object returned by the call is really a
generator object, which doesn't do anything until you iterate over it.
In the case of a coroutine object, there are two basic ways to start
it running: call ``yield from coroutine`` from another coroutine
(assuming the other coroutine is already running!), or convert it to a
Task.
Coroutines can only run when the event loop is running.
Tasks
-----
A Task is an object that manages an independently running coroutine.
The Task interface is the same as the Future interface. The task
becomes done when its coroutine returns or raises an exception; if it
returns a result, that becomes the task's result, if it raises an
exception, that becomes the task's exception.
Canceling a task that's not done yet prevents its coroutine from
completing; in this case an exception is thrown into the coroutine
that it may catch to further handle cancelation, but it doesn't have
to (this is done using the standard ``close()`` method on generators,
described in PEP 342).
The ``par()`` function described above runs coroutines in parallel by
converting them to Tasks. (Arguments that are already Tasks or
Futures are not converted.)
Tasks are also useful for interoperating between coroutines and
callback-based frameworks like Twisted. After converting a coroutine
into a Task, callbacks can be added to the Task.
You may ask, why not convert all coroutines to Tasks? The
``@tulip.coroutine`` decorator could do this. This would slow things
down considerably in the case where one coroutine calls another (and
so on), as waiting for a "bare" coroutine has much less overhead than
waiting for a Future.
The Scheduler
-------------
The scheduler has no public interface. You interact with it by using
``yield from future`` and ``yield from task``. In fact, there is no
single object representing the scheduler -- its behavior is
implemented by the ``Task`` and ``Future`` classes using only the
public interface of the event loop, so it will work with third-party
event loop implementations, too.
Open Issues
===========
@ -617,19 +713,32 @@ Open Issues
yield from sch.block_future(f)
res = f.result()
- Do we need a larger vocabulary of operations for combining
coroutines and/or futures? E.g. in addition to par() we could have
a way to run several coroutines sequentially (returning all results
or passing the result of one to the next and returning the final
result?). We might also introduce explicit locks (though these will
be a bit of a pain to use, as we can't use the ``with lock: block``
syntax). Anyway, I think all of these are easy enough to write
using ``Task``.
- Priorities?
Acknowledgments
===============
Apart from PEP 3153, influences include PEP 380 and Greg Ewing's
tutorial for yield-from, Twisted, Tornado, ZeroMQ, pyftpdlib, tulip
tutorial for ``yield from``, Twisted, Tornado, ZeroMQ, pyftpdlib, tulip
(the author's attempts at synthesis of all these), wattle (Steve
Dower's counter-proposal), numerous discussions on python-ideas from
September through December 2012, a Skype session with Steve Dower and
Dino Viehland, email exchanges with Ben Darnell, an audience with
Niels Provos (original author of libevent), and two in-person meetings
with several Twisted developers, including Glyph, Brian Warner, David
Reid, and Duncan McGreggor.
Reid, and Duncan McGreggor. Also, the author's previous work on async
support in the NDB library for Google App Engine was an important
influence.
Copyright