[Python-Dev] async/await behavior on multiple calls

Andrew Barnert abarnert at yahoo.com
Wed Dec 16 14:35:13 EST 2015


> On Dec 16, 2015, at 03:25, Paul Sokolovsky <pmiscml at gmail.com> wrote:
> 
> Hello,
> 
> On Tue, 15 Dec 2015 17:29:26 -0800
> Roy Williams <rwilliams at lyft.com> wrote:
> 
>> @Kevin correct, that's the point I'd like to discuss.  Most other
>> mainstream languages that implements async/await expose the
>> programming model with Tasks/Futures/Promises as opposed to
>> coroutines  PEP 492 states 'Objects with __await__ method are called
>> Future-like objects in the rest of this PEP.' but their behavior
>> differs from that of Futures in this core way.  Given that most other
>> languages have standardized around async returning a Future as
>> opposed to a coroutine I think it's worth exploring why Python
>> differs.
> 
> Sorry, but what makes you think that it's worth exploring why Python
> Python differs, and not why other languages differ?

They're really the same question.

Python differs from C# in that it builds async on top of language-level coroutines instead of hiding them under the hood, it only requires a simple event loop (which can be trivially built on a select-like function and a loop) rather than a powerful OS/VM-level task scheduler, it's designed to allow pluggable schedulers (maybe even multiple schedulers in one app), it doesn't have a static type system to assist it, ... Turn it around and ask how C# differs from Python and you get the same differences. And there's no value judgment either way.

So, do any of those explain why some Python awaitables aren't safely re-awaitable? Yes: the fact that Python uses language-level coroutines instead of hiding them under the covers means that it makes sense to be able to directly await coroutines (and to make async functions return those coroutines when called), which raises a question that doesn't exist in C#.

What happens when you await an already-consumed awaitables? That question doesn't arise in C# because it doesn't have consumable awaitables. Python _could_ just punt on that by not allowing coroutines to be awaitable, or auto-wrapping them, but that would be giving up a major positive benefit over C#. So, that means Python instead has to decide what happens.

In general, the semantics of awaiting an awaitable are that you get its value or an exception. Can you preserve those semantics even with raw coroutines as awaitables? Sure; as two people have pointed out in this thread, just make awaiting a consumed coroutine raise. Problem solved. But if nobody had asked about the differences between Python and C#, it would have been a lot harder to solve (or even see) the question.

> Also, what "most other languages" do you mean?

Well, what he said was "Most other mainstream languages that implements async/await". But you're right; clearly what he meant was just C#, because that's the only other mainstream language that implements async/await today. Others (JS, Scala) are implementing it or considering doing so, but, just like Python, they're borrowing it from C# anyway. (Unless you want to call F# async blocks and let! binding the same feature--but if so, C# borrowed from F# and everyone else borrowed from C#, so it's still the same.)

> Lua was a pioneer of
> coroutine usage in scripting languages, with research behind that.
> It doesn't have any "futures" or "promises" as part of the language.
> It has only coroutines. For niche cases when "futures" or "promises"
> needed, they can be implemented on top of coroutines.
> 
> And that's actually the problem with Python's asyncio - it tries to
> marry all the orthogonal concurrency concepts, unfortunately good
> deal o'mess ensues.

The fact that futures can be built on top of coroutines, or on top of promises and callbacks, means they're a way to tie together pieces of asynchronous code written in different styles. And the idea of a simple supertype of both futures and coroutines that's sufficient for a large set of problems, means you rarely need wrappers to transform one into the other; just use whichever one you have as an awaitable and it works.

So, you can write 80% of your code in terms of awaitables, but if the last 20% needs to get at the native coroutines, or to integrate with legacy code using callbacks, it's easy to do so. In C#, you instead have to simulate those coroutines with promises even when you're not integrating with legacy code; in a language without futures you'd have to wrap each call into and out of legacy code manually.

If you were designing a new language, you could probably get away with something a lot simpler. (If the only thing you could ever need a future for is to cache an awaitable value, it's a one-liner.) But for Python (and JS, Scala, C#, etc.) that isn't an option.

> It doesn't help on "PR" side too, because coroutine
> lovers blame it for not being based entirely on language's native
> coroutines, strangers from other languages want to twist it to be based
> entirely on foreign concepts like futures, Twisted haters hate that it
> has too much complication taken from Twisted, etc.

There is definitely a PR problem, but I think that's tied directly to the documentation problem, not anything about the design. Unless you've come to things in the same order as Guido, it's hard to figure out even where to dive in to start learning. So you try to write something, fail, get frustrated, and write an angry blog post about why Python asyncio sucks, which actually just exposes your own ignorance of how it works, but since 90% of your readers are just as ignorant of how it works, they believe you're right.

Part of the problem is that there are so many different mediocre paradigms for async programming that each have a million people who sort of know them just well enough to use them. A tutorial that would explain asyncio to someone who's written lots of traditional JS-style callbacks will be useless to someone who's written C-style reactors or Lua-style coroutines. So we probably need a bunch of separate tutorials just to get different classes of people thinking in the right terms before they can read the more detailed documentation.

Also, as with every async design, the first 30 tutorials anyone writes all completely neglect the problem of communicating between tasks (e.g., building a chat server instead of an echo server), so people think that what was easy in their familiar paradigm (because they've gotten used to it, and it's been years since they had to figure it out for themselves because none of the tutorials covered it so they forgot that part) is hard in the new one, and therefore the new one sucks.

>> There's a lot of benefits to making the programming model coroutines
>> without a doubt.  It's absolutely brilliant that I can just call code
>> annotated with @asyncio.coroutine and have it just work.  Code using
>> the old @asyncio.coroutine/yield from syntax should absolutely stay
>> the same. Similarly, since ES7 async/await is backed by Promises
>> it'll just work for any existing code out there using Promises.
>> 
>> My proposal would be to automatically wrap the return value from an
>> `async` function or any object implementing `__await__` in a future
>> with `asyncio.ensure_future()`.  This would allow async/await code to
>> behave in a similar manner to other languages implementing
>> async/await and would remain compatible with existing code using
>> asyncio.
>> 
>> What's your thoughts?
> 
> My thought is "what other languages told when you approached them with
> the proposal to behave like Python?".

I'm pretty sure if you approached the C# team and asked them why re-awaiting a coroutine doesn't produce nil, they'd explain that they deliberately chose not to expose coroutines (actually, I believe they were thinking in terms of continuations, as in F#, but...) under the theory that awaitables are all you'll ever need, which means that problem doesn't come up in the first place. The language can implicitly add such a wrapper and then easily optimize it away when possible because the user never sees inside the wrapper. And if you asked the ES7 committee, they might tell you they actually wanted something closer to Python, but it was just too hard to fit it into their brittle language, so they can't expose awaitables as anything but futures and hope their clever interpreters can optimize out the extra abstraction that you usually don't need, so the question doesn't arise for them either. And if you asked the Scala await fork developers, they'd probably point out that the idiomatic Scala equivalents to returning None and to raising are both returning an empty optional value, so the question doesn't arise for them for a different reason. And in F#, you can build a let!-awaitable out of a raw continuation instead of an async expression, but you have to write the code for that yourself, so you can decide what it does when re-awaited; it's not up to the language or stdlib. And so on. But, even if I'm wrong, and asking those questions would improve those languages, it still wouldn't improve Python.

> Also, wrapping objects in other objects is expensive. Especially if
> the latter kind of objects isn't really needed - it's perfectly
> possibly to write applications which don't use or need any futures at
> all, using just coroutines. Moreover, some people argue that most apps
> real people would write are such, and Futures are niche feature, so
> can't be center of the world.

Well, the whole point of the async model is that most apps real people write only depend on awaitables, and they almost never care whether they're futures or coroutines. This means a language can avoid the overhead of wrapping coroutines in futures (like Python), or keep coroutines out of the user-visible data model (like C#), and work almost the same way.

The problem is that Python is the first mainstream language to adopt awaitables built on top of native, user-visible coroutines, so it has to answer a few questions that C# dodged--like what happens when you await the same coroutine multiple times. That's not a negative judgment on Python, it's just a natural consequence of Python being a little more powerful here than the language it's borrowing from. Refusing to look at the differences between Python and C# would mean not noticing that and leaving it for some future language to solve instead of letting future languages copy from Python (which is always the best way to be consistent with everyone else, of course).




More information about the Python-Dev mailing list