Article #001: asyncio - the flaws
This blog post describes some problems with asyncio that I recently discovered. Knowing about these issues can help you avoid spending days trying to solve non-obvious errors.
Each section is titled with the method whose flaws it addresses.
[1] asyncio.create_task (≥3.7)
If you use asyncio.create_task
without storing a reference to the created object, the task may be garbage collected during its execution. This is because asyncio internally stores created tasks as weak references, which does not increase the reference counter.
This behavior is unclear for many and was not initially documented in Python 3.7 or fixed later. The Python 3.10 documentation proposes an official workaround that we should all follow. My role here is to advertise this issue.
However, the fact remains that the asyncio.create_task
interface could be more intuitive. Many people view a task as something similar to a thread (although their creation differs, as you do not need to store a reference to a thread object).
Recommendations:
- Store references to created tasks using
asyncio.create_task
.
[2] asyncio.gather (≥3.5)
Firstly, note that the asyncio.gather
function returns a special future that does not return True
from .cancelled()
after being cancelled. This is a known, unresolved bug, which is documented at: Issue #85071. Therefore, relying on this function is not recommended.
Secondly, asyncio.gather
has inconsistent error handling. While CancelledError
is propagated to inner tasks, an exception in one of the inner tasks does not cause the cancellation of all other tasks. This behavior is unexpected and makes the interface difficult to use. In order to cancel those tasks on your own, you must create them separately. Passing bare coroutines can be risky as you won’t have a reference to cancel those coroutines once they are converted to tasks inside asyncio.gather
.
I would say that asyncio.gather
should not be used without a proper try-except
block.
Recommendations:
- <py3.11: do not rely on the
.cancelled()
method of the future object returned byasyncio.gather
. - <py3.11: pass tasks to
asyncio.gather
instead of coroutine objects. - <py3.11: use
asyncio.gather
with atry-except
block to cancel gathered tasks in case of an error. - ≥py3.11: use
asyncio.TaskGroup
instead ofasyncio.gather
.
[3] asyncio.wait_for (≥3.5,<3.12)
asyncio.wait_for
swallows asyncio.CancelledError
on a very rare race condition, causing unexpected code execution continuation. This behavior is known as Issue #86296 in the CPython repository.
Fortunately, it was fixed very recently in Python 3.12.
Unrelated trivia about
asyncio.wait_for
is that some people considered its implementation to be too complex. As an alternative, they suggested replacing its internal implementation withasyncio.timeout
: Issue #96764.
Recommendations:
- <py3.11: use
asyncio.wait
instead ofasyncio.wait_for
(as it also supportstimeout=
parameter, so the same goal can be achieved). - =py3.11: use
asyncio.timeout
instead ofasyncio.wait_for
.