[Python-ideas] Late to the async party (PEP 3156)
Jason Tackaberry
tack at urandom.ca
Sun Dec 16 02:36:18 CET 2012
Hi python-ideas,
I've been somewhat living under a rock for the past few months and
consequently I missed the ideal window of opportunity to weigh in on the
async discussions this fall that culminated into PEP 3156.
I've been reading through those discussions in the archives. I've not
finished digesting it all, and I'm somewhat torn in that I feel I should
shut up until I read everything to date so as not to decrease the SNR,
but on the other hand, knowing myself, I strongly suspect this would
result in my never speaking up. And so, at risk of lowering the SNR ...
First let me say that PEP 3156 makes me very, very happy.
Over the past few years I've been exploring these very ideas with a
little-used library called Kaa. I'm not offering it up as a paragon of
proper async library design, but I wanted to share some of my
experiences in case they could be useful to the PEP.
https://github.com/freevo/kaa-base/
http://api.freevo.org/kaa-base/
It does seem like many similar design choices were made. In particular,
I'm happy that an explicit yield will be used rather than the greenlet
style of implicit suspension/reentry. Even after I've been using them
for years, coroutines often feel like a form of magic, and an explicit
yield is more aligned with the principle of least surprise.
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
o 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.)
o 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.)
o 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.)
o 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
o discussion in the PEP on cancellation has some TBDs so perhaps
the above will be food for thought
* 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
o 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)
o I didn't see much discussion in the PEP on timeouts, but I think
this is an important feature that should be standardized
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.
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
* 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
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.
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.
Cheers,
Jason.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/python-ideas/attachments/20121215/802f41fe/attachment.html>
More information about the Python-ideas
mailing list