Race conditions with asyncio
in Python
What do you think the output of the following code will be?
We might guess:
With the explanation being:
- First, we schedule
should_only_print_once
to run 3 times in theTaskGroup
. - The first time the event loop enters the function,
ran = False
is printed. - Since
ran
isFalse
, we enter theif
conditional and setran = True
. - Print
after ran = True
. - For the remaining times the event loop enters the function,
ran
is nowFalse
. So, we printran = False
twice and end.
In fact, the output is:
What is happening here?
Asynchronous Execution
At line 13, we have await asyncio.sleep(0)
.
This yields control flow to the event loop. At this point, the event loop is free to continue executing other tasks, such as the other 2 tasks for foo()
.
Importantly, at this point, the value of ran
is still False
- the initial run has not yet reached ran = True
. Therefore, when the event loop reenters foo()
subsequently, it sees that ran == False
.
Finally, when all 3 asynchronous executions of foo()
have reached await asyncio.sleep(0)
, the first one to run sets ran = True
, and that is what the subsequent executions see and print.
Fixing the issue
The correct way to fix this is by wrapping code that accesses shared state in an asyncio.Lock
.
Output:
When the first task for foo()
executes, it acquires the lock
and gains exclusive access to the subsequent code.
During the await
at line 8, control flow switches to the other tasks executing foo()
, but because the lock was already acquired by the first task, they cannot proceed further.
Execution finally returns to the first task, which sets ran = True
and completes, allowing the rest of the waiting tasks to execute in the same fashion.