[Python-ideas] Late to the async party (PEP 3156)

Guido van Rossum guido at python.org
Sun Dec 16 17:27:53 CET 2012


On Sat, Dec 15, 2012 at 5:36 PM, Jason Tackaberry <tack at urandom.ca> wrote:

> With Kaa, our future-style object is called an InProgress (so forgive the
> differing terminology in the remainder of this post):
>
>     http://api.freevo.org/kaa-base/async/inprogress.html
>
> A couple properties of InProgress objects that I've found have practical
> value:
>
>    - they can be aborted, which raises a special InProgressAborted inside
>    the coroutine function so it can perform cleanup actions
>       - what makes this tricky is the question of what to do to any
>       currently yielded tasks?  If A yields B and A is aborted, should B be
>       aborted?  What if the same B task is being yielded by C?  Should C also be
>       aborted, even if it's considered a sibling of A?  (For example, suppose B
>       is a task that is refreshing some common cache that both A and C want to
>       make sure is up-to-date before they move on.)
>       - if the decision is B should be aborted, then within A, 'yield B'
>       will raise an exception because A is aborted, but 'yield B' within C will
>       raise because B was aborted.  So there needs to be some mechanism to
>       distinguish between these cases.  (My approach was to have an origin
>       attribute on the exception.)
>        - if A yields B, it may want to prevent B from being aborted if A
>       is aborted.  (My approach was to have a noabort() method in InProgress
>       objects to return a new, unabortable InProgress object that A can then
>       yield.)
>       - alternatively, the saner implementation may be to do nothing to B
>       when A is aborted and require A catch InProgressAborted and explicitly
>       abort B if that's the desired behaviour
>       - discussion in the PEP on cancellation has some TBDs so perhaps
>       the above will be food for thought
>
> The PEP is definitely weak. Here are some thoughts/proposals though:

   - You can't cancel a coroutine; however you can cancel a Task, which is
   a Future wrapping a stack of coroutines linked via yield-from.
   - Cancellation only takes effect when a task is suspended.
   - When you cancel a Task, the most deeply nested coroutine (the one that
   caused it to be suspended) receives a special exception (I propose to reuse
   concurrent.futures.CancelledError from PEP 3148). If it doesn't catch this
   it bubbles all the way to the Task, and then out from there.
   - However when a coroutine in one Task uses yield-from to wait for
   another Task, the latter does not automatically get cancelled. So this is a
   difference between "yield from foo()" and "yield from Task(foo())", which
   otherwise behave pretty similarly. Of course the first Task could catch the
   exception and cancel the second task -- that is its responsibility though
   and not the default behavior.
   - PEP 3156 has a par() helper which lets you block for multiple
   tasks/coroutines in parallel. It takes arguments which are either
   coroutines, Tasks, or other Futures; it wraps the coroutines in Tasks to
   run them independently an just waits for the other arguments. Proposal:
   when the Task containing the par() call is cancelled, the par() call
   intercepts the cancellation and by default cancels those coroutines that
   were passed in "bare" but not the arguments that were passed in as Tasks or
   Futures. Some keyword argument to par() may be used to change this behavior
   to "cancel none" or "cancel all" (exact API spec TBD).



>    - they have a timeout() method, which returns a new InProgress object
>    representing the task that will abort when the timeout elapses if the task
>    doesn't finish
>       - it's noteworthy that timeout() returns a *new* InProgress and the
>       original task continues on even if the timeout occurs -- by default that
>       is, unless you do timeout(abort=True)
>        - I didn't see much discussion in the PEP on timeouts, but I think
>       this is an important feature that should be standardized
>
> Interesting. In Tulip v1 (the experimental version I wrote before PEP
3156) the Task() constructor has an optional timeout argument. It works by
scheduling a callback at the given time in the future, and the callback
simply cancel the task (which is a no-op if the task has already
completed). It works okay, except it generates tracebacks that are
sometimes logged and sometimes not properly caught -- though some of that
may be my messy test code. The exception raised by a timeout is the same
CancelledError, which is somewhat confusing. I wonder if Task.cancel()
shouldn't take an exception with which to cancel the task with.
(TimeoutError in PEP 3148 has a different role, it is when the timeout on a
specific wait expires, so e.g. fut.result(timeout=2) waits up to 2 seconds
for fut to complete, and if not, the call raises TimeoutError, but the code
running in the executor is unaffected.)



>
> Coroutines in Kaa use "yield" rather than "yield from" but the general
> approach looks very similar to what's been proposed:
>
>     http://api.freevo.org/kaa-base/async/coroutines.html
>
> The @coroutine decorator causes the decorated function to return an
> InProgress.  Coroutines can of course yield other coroutines, but, more
> fundamentally, anything else that returns an InProgress object, which could
> be a @threaded function, or even an ordinary function that explicitly
> creates and returns an InProgress object.
>

We've had long discussions about yield vs. yield-from. The latter is way
more efficient and that's enough for me to push it through. When using
yield, each yield causes you to bounce to the scheduler, which has to do a
lot of work to decide what to do next, even if that is just resuming the
suspended generator; and the scheduler is responsible for keeping track of
the stack of generators. When using yield-from, calling another coroutine
as a subroutine is almost free and doesn't involve the scheduler at all;
thus it's much cheaper, and the scheduler can be simpler (doesn't need to
keep track of the stack). Also stack traces and debugging are better.


>
> There are some features of Kaa's implementation that could be worth
> considering:
>
>    - it is possible to yield a special object (called NotFinished) that
>    allows a coroutine to "time slice" as a form of cooperative multitasking
>
> I can recommend yield from tulip.sleep(0) for that.


>
>    - coroutines can have certain policies that control invocation
>    behaviour.  The most obvious ones to describe are POLICY_SYNCHRONIZED which
>    ensures that multiple invocations of the same coroutine are serialized, and
>    POLICY_SINGLETON which effectively ignores subsequent invocations if it's
>    already running
>    - it is possible to have a special progress object passed into the
>    coroutine function so that the coroutine's progress can be communicated to
>    an outside observer
>
>
These seem pretty esoteric and can probably implemented in user code if
needed.


>
>
>
> Once you've standardized on a way to manage the lifecycle of an
> in-progress asynchronous task, threads are a natural extension:
>
>     http://api.freevo.org/kaa-base/async/threads.html
>
> The important element here is that @threaded decorated functions can be
> yielded by coroutines.  This means that truly blocking tasks can be wrapped
> in a thread but invocation from a coroutine is identical to any other
> coroutine.  Consequently, a threaded task could later be implemented as a
> coroutine (or more generally via event loop hooks) without any API changes.
>

As I said, I think wait_for_future() and run_in_executor() in the PEP give
you all you need. The @threaded decorator you propose is just sugar; if a
user wants to take an existing API and convert it from a coroutine to
threaded without requiring changes to the caller, they can just introduce a
helper that is run in a thread with run_in_executor().


> I think I'll stop here.  There's plenty more definition, discussion, and
> examples in the links above.  Hopefully some ideas can be salvaged for PEP
> 3156, but even if that's not the case, I'll be happy to know they were
> considered and rejected rather than not considered at all.
>

Thanks for your very useful contribution! Kaa looks like an interesting
system. Is it ported to Python 3 yet? Maybe you could look into integrating
with the PEP 3156 event loop and/or scheduler.

-- 
--Guido van Rossum (python.org/~guido)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/python-ideas/attachments/20121216/c235c3c1/attachment.html>


More information about the Python-ideas mailing list