What is going on behind the scenes that results in this behavior
The simplest answer is that asyncio is based on cooperative multitasking, and time.sleep
doesn't cooperate. time.sleep(2)
blocks the thread for two seconds, the event loop and all, and there is nothing anyone can do about it.
On the other hand, asyncio.sleep
is carefully written so that when you await asyncio.sleep(2)
, it immediately suspends the current task and arranges with the event loop to resume it 2 seconds later. Asyncio's "sleeping" is implicit, which allows the event loop to proceed with other tasks while the coroutine is suspended. The same suspension system allows wait_for
to cancel the task, which the event loop accomplishes by "resuming" it in such await that the await
where it was suspended raises an exception.
In general, a coroutine not awaiting anything is good indication that it's incorrectly written and is a coroutine in name only. Awaits are the reason coroutines exist, and sleeper
doesn't contain any.
is there a way to modify behavior to match my intuition?
If you must call legacy blocking code from asyncio, use run_in_executor
. You will have to tell asyncio when you do so and allow it to execute the actual blocking call, like this:
async def sleeper():
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, time.sleep, 2)
time.sleep
(or other blocking function) will be handed off to a separate thread, and sleeper
will get suspended, to be resumed when time.sleep
is done. Unlike with asyncio.sleep()
, the blocking time.sleep(2)
will still get called and block its thread for 2 seconds, but that will not affect the event loop, which will go about its business similar to how it did when await asyncio.sleep()
was used.
Note that cancelling a coroutine that awaits run_in_executor
will only cancel the waiting for the blocking time.sleep(2)
to complete in the other thread. The blocking call will continue executing until completion, which is to be expected since there is no general mechanism to interrupt it.