This sample code hangs indefinitely:
import asyncio
async def main():
async def f():
await g_task
async def g():
await f_task
f_task = asyncio.create_task(f())
g_task = asyncio.create_task(g())
await f_task
asyncio.run(main())
I'm looking for a way to automatically detect and handle deadlocks, like GoLang does.
So far I came up with a variant of asyncio.wait_for()
:
[EDIT] overhauled design
https://gist.github.com/gimperiale/549cbad04c24d870145d3f38fbb8e6f0
1 line change in the original code:
await wait_check_deadlock(f_task)
It works, but with two major problems:
asyncio.Task._fut_waiter
, which is an implementation detail of CPythonaw.cancel()
seems to do nothing. If I catch the RecursionError my helper function raises, asyncio.run() raises another RecursionError when it tries cancelling all tasks.Are there more robust solutions to the problem?
That would be the end of story but sometimes it cannot be avoided, and it’s not the only case. Deadlock might also be cause by other sort of blocking code, waiting for semaphore, acquiring as lock. The advice in general is simple. Don’t block in async code.
Deadlocked processes are involved in a circular chain such that each process holds one or more resources being requested by the next process in the chain. There are three approaches to deal with deadlocks. 1. Deadlock Prevention 2. Deadlock avoidance 3. Deadlock detection These are explained as following below. 1. Deadlock Prevention :
The following are the strategies used for Deadlock Handling in Distributed System: 1. Deadlock Prevention: As the name implies, this strategy ensures that deadlock can never happen because system designing is carried out in such a way. If any one of the deadlock-causing conditions is not met then deadlock can be prevented.
When developing a new I/O bound application from scratch, asyncio may be a natural technology choice. Starting out, you’ll be able to use non-blocking libraries that work with asyncio such as asyncpg and aiohttp as you begin development. However, greenfields development is a luxury that many developers don’t have.
Deadlock avoidance has been researched a lot, some practical solutions exist, but in general case, the problem is undecidable (I think it can be reduced to the halting problem).
To illustrate practicality, consider this:
await asyncio.sleep(2 ** (1 / random.random()))
Depending on your luck, it will either return soon or "practically never".
This trick can be used to show that callback-based program is impossible to predict:
f = asyncio.Future()
async foo():
await asyncio.sleep(2 ** (1 / random.random()))
f.set_result(None)
async bar():
await f
await asyncio.gather(foo(), bar())
Likewise, it can be applied to your "pure" async/await program:
async def f():
await g_task
async def g():
await asyncio.wait(f_task,
asyncio.sleep(2 ** (1 / random.random())),
return_when=asyncio.FIRST_COMPLETED)
f_task = asyncio.create_task(f())
g_task = asyncio.create_task(g())
await f_task
At the same time, imperfect but practical deadlock detector can be very useful, please consider posting your code to core asyncio devs and/or a standalone library.
The current practice is to run tests with PYTHONASYNCIODEBUG=1
which shows unawaited tasks (destroyed before result / exception was read).
Your library could be better, for example, it could report when some task took longer than X, or when a DAG of tasks depending on given task grows too large.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With