Why does python's async execution model so strictly separate synchronous from asynchronous functions?
I'm very new to async programming and I'm struggling to understand the design decisions behind python's implementation of async. What confuses me most is why asynchronous functions (defined with async def) return a coroutine that needs to be awaited instead of just returning the result of that coroutine. In the world of synchronous programming, you could rely on the fact that calling a function would return its result. But now that async functions are a thing, you always have to know whether a function is synchronous or asynchronous because calling an asynchronous function does something fundamentally different than calling a synchronous function. I don't see a reason why this strict separation of sync/async functions is necessary. Why can't calling a function mean "I want to get the result of this function"? Why can't we write code that doesn't care whether the functions it's calling are synchronous or asynchronous?
If I were to design an async execution model, I'd change two things:
- Get rid of the
awaitkeyword and implicitly await all asynchronous functions when they're called. In order to run multiple asynchronous functions simultaneously, we would use futures. - Get rid of
async def,async withandasync for.
Here's an example of how this would work:
def wait_and_print():
time.sleep(1)
print("success")
def i_dont_care_about_async():
# both of these should wait 1 second and then print "success"
wait_and_print()
wait_and_print()
def i_want_to_be_async():
# this should wait 1 second and then print "success" twice
future1 = Future(wait_and_print)
future2 = Future(wait_and_print)
asyncio.run(asyncio.gather(future1, future2))
As you can see, I defined a normal (synchronous, if you will) function wait_and_return. Calling this function normally makes it block for 1 second before returning a value. But if we want to, we can use a Future to run multiple instances of this function simultaneously. This isn't possible in python - in python I would have to implement an asynchronous copy of wait_and_return with async def and asyncio.sleep.
Of course, all of this is only possible if an implicit event loop is running in the background and time.sleep yields execution to that event loop. Surely this would be much more convenient than python's current async model? Is there any reason why async wasn't implemented like this? Is there some kind of problem with this model, or maybe even something that makes this impossible to implement in practice?
54 comments · 81% upvoted · other discussions
I just ported a large project (50K LOC) completely to async. I changed thousands of lines of code.
First, I love the async model and everything about it.
However, I really shouldn't have had to do all that work. It isn't just adding a bunch of
asyncandawaiteverywhere. It is converting everyforloop into anasync forloop. It is converting every context manager and list comprehension into an async one. It is convertingyield fromback into regular loops. It is rearchitecting things if you have a generator expression. Essentially there are two syntaxes now, one for async functions and one for non-async, and they haven't transitioned all the syntax from non-async to async yet.Essentially, when you mark a function as async, you're telling the interpreter that someone, maybe 7 layers deep, might wait on a OS object. You could have 99% of your code be non-async, but because of that one dude (probably waiting on a socket), thousands of lines of code have to change. This viral nature of async is bad. I don't mind marking the functions, I do mind converting code from one syntax to another simply because it makes the interpreter slightly easier to implement.
If an async function calls another async function, the await should be implicit. There should be some other keyword to *NOT* await on a called coroutine. In this way, you wouldn't have to rewrite every function.
dunkler_wanderer 7 points · Nov 2018
Relevant article/rant: What Color is Your Function?
Zomunieo 6 points · Nov 2018
The alternative asyncio package trio seems to avoid the viral infection problem. It adds a method that allows ordinary function to call an async function, so instead of changing ever layer of an existing code base you at most have to change the point that does I/O.
devxpy 2 points · Nov 2018
method that allows ordinary function to call an async function
Please more background on how this is implemented?
Zomunieo 2 points · Nov 2018
As I understand it trio provides its own implementation of an asyncio event loop which is designed in a way that makes this possible. The case it solves an async function that calls into sync code that then needs to do something asynchronous. Say you pass a callback function into a library and the callback is async. So trio could walk up the call stack and find some async context to figure out which task needs to be suspended.
devxpy 2 points · Nov 2018
Wait, are you talking about
trio.run()? Becauseasyncio.run()also exists now. What's really the difference between the two?Another serious question - Why is this so useful? I have little async experience (mostly just asyncio), but isn't the primary concern with async the other way round?
As in you accidentally call a "sync" function from an "async" code, and then that "sync" code ends up hogging your event loop. Or of course, the unfortunate case where an "async" version doesn't exist for the task you're trying to achieve.
I realize that trio is relatively easier to use and debug than asyncio, but don't see any technical improvements it brings, over the other.
Could you please provide some more background?
Zomunieo 1 points · Nov 2018
You've got more detailed questions than I have answers for. My contact with trio is pretty superficial.
devxpy 1 points · Nov 2018
Goddamnit. Where do I find people who have these answers!
Zomunieo 1 points · Nov 2018
You could try the python-trio gitter.im chatroom or asking the author.
devxpy 2 points · Nov 2018
Thanks, but I meant it like a angry statement referring to the plethora of un-answered questions in computer science (for me atleast) xD
caleb 3 points · Nov 2018
This doesn't sound right. You should only need async iterators and context managers if the special methods invoked by them are required to be awaitables. For example, it's fine for the "body" of a sync context manager to use await.