|
|
Subscribe / Log in / New account

Python coroutines with async and await

By Jake Edge
May 13, 2015

It is already possible to create coroutines for asynchronous processing in Python. But a recent proposal would elevate coroutines to a full-fledged language construct, rather than treat them as a type of generator as they are currently. Two new keywords, async and await, would be added to the language to support coroutines as first-class Python features.

A coroutine is a kind of function that can suspend and resume its execution at various pre-defined locations in its code. Subroutines are a special case of coroutines that have just a single entry point and complete their execution by returning to their caller. Python's coroutines (both the existing generator-based and the newly proposed variety) are not fully general, either, since they can only transfer control back to their caller when suspending their execution, as opposed to switching to some other coroutine as they can in the general case. When coupled with an event loop, coroutines can be used to do asynchronous processing, I/O in particular.

Python's current coroutine support is based on the enhanced generators from PEP 342, which was adopted into Python 2.5. That PEP changed the yield statement to be an expression, added several new methods for generators (send(), throw(), and close()), and ensured that close() would be called when generators get garbage-collected. That functionality was further enhanced in Python 3.3 with PEP 380, which added the yield from expression to allow a generator to delegate some of its functionality to another generator (i.e. a sub-generator).

But all of that ties coroutines to generators, which can be confusing and also limits where in the code it is legal to make an asynchronous call. In particular, the with and for statements could conceptually use an asynchronous call to a coroutine, but cannot because the language syntax does not allow yield expressions in those locations. In addition, if a refactoring of the coroutine moves the yield or yield from out of the function (into a called function, for example), it no longer is treated as a coroutine, which can lead to non-obvious errors; the asyncio module works around this deficiency by using a @asyncio.coroutine decorator.

PEP 492 is meant to address all of those issues. The ideas behind it were first raised by Yury Selivanov on the python-ideas mailing list in mid-April, it was enthusiastically embraced by many in that thread, and by May 5 it had been accepted for Python 3.5 by Guido van Rossum. Not only that, but the implementation was merged on May 12. It all moved rather quickly, though it was discussed at length in multiple threads on both python-ideas and python-dev.

The changes are fairly straightforward from a syntax point of view:

    async def read_data(db):
        data = await db.fetch('SELECT ...')
	...
That example (which comes from the PEP) would create a read_data() coroutine using the new async def construct. The await expression would suspend execution of read_data() until the db.fetch() awaitable completes and returns its result. await is similar to yield from, but it validates that its argument is an awaitable.

There are several different types of awaitable. A native coroutine object, as returned by calling a native coroutine (i.e. one defined with async def) is an awaitable, as is a generator-based coroutine that has been decorated with @types.coroutine. Future objects, which represent some processing that will complete in the future, are also awaitable. The __await__() magic method is present for objects that are awaitable.

There is a problem that occurs when adding new keywords to a language, however. Any variables that are named the same as the keyword suddenly turn into syntax errors. To avoid that problem, Python 3.5 and 3.6 will "softly deprecate" async and await as variable names, but not have them be a syntax error. The parser will keep track of async def blocks and treat the keywords differently within those blocks, which will allow existing uses to continue to function.

There are two other uses of async that will come with the new feature: asynchronous context managers (i.e. with) and iterators (i.e. for). Inside a coroutine, these two constructs can be used as shown in these examples from the PEP:

    async def commit(session, data):
	...

	async with session.transaction():
	    ...
	    await session.update(data)
	    ...
        ...
        async for row in Cursor():
            print(row)
Asynchronous context managers must implement two magic async methods, __aenter__() and __aexit__(), both of which return awaitables, while an asynchronous iterator would implement __aiter__() and __anext__(). Those are effectively the asynchronous versions of the magic methods used by the existing synchronous context manager and iterator.

The main question early on was whether the deferred "cofunction" feature (PEP 3152) might be a better starting point. The author of that PEP, Greg Ewing, raised the issue, but there was a lot of agreement that the syntax proposed by Selivanov was preferable to the codef, cocall, and the like from Ewing's proposal. There was a fair amount of back and forth, but the cofunction syntax for handling certain cases got rather complex and non-Pythonic in the eyes of some. Van Rossum summarized the problems with cofunctions while rejecting that approach.

There were also several suggestions of additional asynchronous features that could be added, but nothing that seemed too urgent. There was some bikeshedding on the keywords (and their order, some liked def async, for example). The precedence of await was also debated at some length, with the result being that, unlike yield and yield from that have the lowest precedence, await has a high precedence: between exponentiation and subscripting, calls, and attribute references.

Mark Shannon complained that there was no need to add new syntax to do what Selivanov was proposing. Others had made similar observations and it was not disputed by Selivanov or other proponents. The idea is to make it easier to program with coroutines. Beyond that, Van Rossum wants the places where a coroutine can be suspended to be obvious from reading the code:

But new syntax is the whole point of the PEP. I want to be able to *syntactically* tell where the suspension points are in coroutines. Currently this means looking for yield [from]; PEP 492 just adds looking for await and async [for|with]. Making await() a function defeats the purpose because now aliasing can hide its presence, and we're back in the land of gevent or stackless (where *anything* can potentially suspend the current task). I don't want to live in that land.

Over a two to three week period, multiple versions of the PEP were posted and debated, with Selivanov patiently explaining his ideas or modifying them based on the feedback. For a feature that seems likely to be quite important in Python's future, the whole process went remarkably quickly—and smoothly. It will probably take a fair amount more time for those ideas to sink in more widely with Python developers.



to post comments

Python coroutines with async and await

Posted May 14, 2015 3:28 UTC (Thu) by fsateler (subscriber, #65497) [Link] (3 responses)

This looks a lot like the .Net async/await support (which I guess is no surprise given that it is a pretty successful model so good ideas can be copied). This (the python) spec has the added plus that you can loop async lists, which is something missing in the .Net world.

It wasn't clear though if it was possible to execute more than one awaitable at a time and then await some or all of them. For example, issue 2 async calls to different databases and then await on both to combine the result, so the calls execute in parallel. I would presume yes, but reading specs is not quite my strength, and all the examples I saw had the await keyword specified when calling an async method, which would serialize async calls.

Python coroutines with async and await

Posted May 15, 2015 9:23 UTC (Fri) by andreashappe (subscriber, #4810) [Link]

Odersky (Scala), Meijer (.net) and Kuhn (Akka) are currently doing a coursera course about reactive programming -- might be handy as background information as async/await is featured in the (free) course.

https://www.coursera.org/course/reactive

Python coroutines with async and await

Posted May 16, 2015 8:25 UTC (Sat) by HIGHGuY (subscriber, #62277) [Link]

Yes, it does:

>This PEP assumes that the asynchronous tasks are scheduled and coordinated
>by an Event Loop similar to that of stdlib module
>asyncio.events.AbstractEventLoop . While the PEP is not tied to any specific
>Event Loop implementation, it is relevant only to the kind of coroutine that uses
>"yield" as a signal to the scheduler, indicating that the coroutine will be waiting
>until an event (such as IO) is completed.

So upon await, it schedules back to the eventloop and allows another query to be started.

Python coroutines with async and await

Posted May 25, 2015 11:18 UTC (Mon) by gdamjan (subscriber, #33634) [Link]

> It wasn't clear though if it was possible to execute more than one awaitable at a time and then await some or all of them.

I believe you'd do that with asyncio.wait. I assume it would look like this (not tested):

fut1 = db1.fetch('SELECT ...')
fut2 = db2.fetch('SELECT ...')
done, pending = await asyncio.wait([fut1, fut2])

see asyncio.wait docs for how to do 'all' vs 'some'.

Python coroutines with async and await

Posted May 15, 2015 16:25 UTC (Fri) by sorokin (guest, #88478) [Link] (1 responses)

I know Microsoft has two patents about async/await:

http://www.freepatentsonline.com/y2012/0324431.html
http://www.freepatentsonline.com/y2012/0324457.html

I don't know the details about these patents. (Actually I have not read them). And I'm not a lawyer. But isn't it dangerous to implement a patent-encumbered feature in a programming language? Is it true that Microsoft will be able to seek royalty from Red Hat (or from everybody who distribute a interpreter) if Red Hat ships Python with async/await support?

Python coroutines with async and await

Posted May 15, 2015 16:31 UTC (Fri) by Cyberax (✭ supporter ✭, #52523) [Link]

Async/await were first implemented in ML sometimes in the early 80-s. It's hardly new.

Python coroutines with async and await

Posted Jun 8, 2015 11:25 UTC (Mon) by RasmusWL (guest, #101814) [Link]

The article says

> Python's coroutines (both the existing generator-based and the newly proposed variety) are not fully general, either, since they can only transfer control back to their caller when suspending their execution, as opposed to switching to some other coroutine as they can in the general case.

In the paper "Revisiting coroutines" from 2009 (https://dl.acm.org/citation.cfm?id=1462167), it is shown that the two forms of coroutines (passing to any coroutine / passing to parent) are equivalent in expressive power. They actually recommend the other kind. So from my perspective, Python is doing quite good :)


Copyright © 2015, Eklektix, Inc.
This article may be redistributed under the terms of the Creative Commons CC BY-SA 4.0 license
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds