Issues with asyncio¶
Structured Concurrency¶
Whenever a coroutine function is run, it might or might not spawn some background task. The function seemed to return, but is it still running in the background? There’s no way to know without reading all its source code, transitively. When will it finish? Hard to say.
If an error occurs in a background task, and you don’t handle it manually, then the runtime just… drops it on the floor and crosses its fingers that it wasn’t too important. If you’re lucky it might print something on the console.
When running a coroutine function it always may create a Task
and the code
execution is forked actually. This Task
can be passed around, maybe cancelled,
raise exceptions and may even create additional Task
s. All this may be hidden
to the caller. Nothing ensures that the Task
execution is joined later on.
import asyncio
async def coro():
try:
await asyncio.sleep(60)
return 42
except asyncio.CancelledError:
print("someone wants to cancel me")
raise
async def other_coro():
return asyncio.create_task(coro())
task = asyncio.run(other_coro())
print(task)
Output:
>>> task = asyncio.run(other_coro())
someone wants to cancel me
>>> print(task)
<Task cancelled name='Task-22' coro=<coro() done, defined at <stdin>:1>>
The task for coro
was never awaited. It gets canceled when the loop (the
asyncio.run
function) finishes.
Different async based libraries are addressing this issue:
This issue has been fixed in Python 3.11 with the introduction of TaskGroups.
Before:
async def coro():
task1 = asyncio.create_task(some_coro(...))
task2 = asyncio.create_task(another_coro(...))
# join/await all tasks explicitly
await asyncio.gather(task1, task2)
print("Both tasks have completed now.")
With Python 3.11:
async def coro():
async with asyncio.TaskGroup() as tg:
task1 = tg.create_task(some_coro(...))
task2 = tg.create_task(another_coro(...))
# tasks are joined/awaited automatically
print("Both tasks have completed now.")
asyncio is not the only async library¶
Every async library call flow in Python must work like this
async-lib-x
-> coroutine
-> ...
-> coroutine
-> async-lib-x
You might know already the reason for this. Every async library needs a loop and the loop needs to track all jobs, tasks, etc. Therefore it needs to track also all I/O to notify waiting coroutines.
So if you are an author of a networking protocol library you need to support
async-lib-x
explicitly to work correctly with your library.
To name the major ones:
Supporting all of them seems to be a big burden when you only want to get faster code with async/await.
Personal Advice
Just stick with asyncio. It is a standard Python library and is still relatively
new. During the years it got improved with every Python release. Most async
libraries will only support asyncio and with TaskGroups
a major downside is
already resolved.
Coloured Functions¶
In What Color is Your Function? the author argues that every function has a color. There a no functions without a color. He introduced blue and red colored functions. You can call a blue function from within a red one but you can’t call a red function from within a blue one. That means red functions are getting more painful to handle then blue ones. Best would be to just have blue functions overall but sadly some core functions are red. Thus you can’t avoid them.
Of course this is only an analogy of async and sync functions. Let’s take a look at the following table:
If a function like this |
wants to call a function like this |
does it work? |
---|---|---|
sync |
sync |
✓ |
sync |
async |
X |
async |
sync |
✓ |
async |
async |
✓ |
Remember the Intermediate Function Issue. It’s still the
same with native coroutine functions. You still can’t call async functions from
synchronous ones. The async/await
syntax didn’t change the situation.
And think about this and asyncio-is-not-the-only-async-library again. Therefore as also mentioned in What Color is Your Python async Library? we have much more then two colors in Python.
And you just can’t implement all these colors under the same namespace:
class SomeClass:
def same_name(self): # a function (method)
return
def same_name(self): # a generator function
yield
async def same_name(self): # a coroutine function
await smth()
async def same_name(self): # an asynchronous generator function
await smth()
yield 42
async def same_name(self): # a coroutine function using a different async lib
await asynclibx.smth()
and async-lib-x
-> coroutine
-> ...
-> coroutine
-> async-lib-y
will
not work at all too.