Hey, did you know that unless you know exactly what you’re doing, you should never use asyncio.gather
after manually creating async tasks via asyncio.create_task()
?
This code,
is deeply unsafe. Let me cite the (in)famous article that alerted me to this problem, the Heisenbug lurking in your async code. Here’s also an excellent stack overflow answer that goes a bit more in depth. In short, due to python’s garbage collector, those task_*
objects we created are weak references. Python’s garbage collector doesn’t understand that those task_*
objects have a life after the asyncio.gather
, and they may just be arbitrarily garbage collected by python, and never run.
Why does python do this? ¯\_(ツ)_/¯. I would like to have a cordial conversation to whoever designed it this way.
In fact, the asyncio docs for create_task
have a warning for this:
Alright, fair, but I, like hundreds of millions developers, will skip text that’s in a grey box. It needs to have red scary text, maybe outlined in red, and it should also have a popup in the browser. Guido Van Rossum should mail each IP address that has ever downloaded python a hand-written letter warning them of this. That’s how serious this problem is.
The alternative solution, as the docs mention, is to use asyncio.TaskGroup
. I actually love the asyncio.TaskGroup()
abstraction, and it serves its purpose well.
Pretty good! After the tg
scope context manager has ended, we are guaranteed that each task has finished (or errored). Even though I have strong feelings about python not really having true scoping, we are able to consume the results of those tasks. But it still is a bit un-ergonomic. What if we want to create 1 million tasks, all with the same function, and get the results of all of them simultaneously?
Nicer, but now let’s really abstract it.
Then, we can use it:
Interestingly, mypy
throws an error on the [tg.create_task(coro) for coro in coros]
block, claiming that Argument 1 to "create_task" of "TaskGroup" has incompatible type "Awaitable[T]"; expected "Coroutine[Any, Any, Any]. Mypy[arg-type]
. But… it’s right.