asyncio.sleep() vs. time.sleep(), and create_task()
time.sleep() inside a coroutine blocks everything
This is the second “show the pain” moment worth taking seriously: using
time.sleep() — the regular, blocking version — inside an async def
function doesn’t just fail to be concurrent for that one call. It blocks
the entire event loop, meaning nothing else scheduled through
asyncio can make progress either, even other coroutines running
alongside it in the same gather():
import asyncio
import time
async def call_search_api():
print("calling search API...")
time.sleep(2) # blocking — not await asyncio.sleep(2)
return "3 results found"
async def call_weather_api():
print("calling weather API...")
await asyncio.sleep(1)
return "72F and sunny"
async def main():
return await asyncio.gather(call_search_api(), call_weather_api())
start = time.perf_counter()
results = asyncio.run(main())
elapsed = time.perf_counter() - start
print(f"total time: {elapsed:.1f}s")calling search API...
calling weather API...
total time: 3.0s(illustrating expected behavior on a real machine — this demo relies on
time.sleep()’s blocking behavior, which isn’t reliable in this
particular in-browser sandbox; see the callout at the end of this
section)
Roughly 3.0 seconds — 2 + 1, not the ~2.0 seconds you’d expect from
gather() running both concurrently. What happened: call_search_api()
started, then hit time.sleep(2) — a call that has no idea an event loop
even exists, and simply freezes the entire program for two seconds,
call_weather_api() included, even though call_weather_api() itself
was written correctly with await asyncio.sleep(1). One incorrectly
blocking call inside an async function poisons the concurrency for
everything else sharing that event loop, not just its own coroutine.
The fix is simply using await asyncio.sleep(...) everywhere a
coroutine needs to pause — never the plain, blocking time.sleep(). This
is the practical rule worth internalizing: inside async def code, a
blocking call of any kind (a blocking sleep, a blocking network
request from a non-async library, blocking disk I/O) has this same
poisoning effect on everything else scheduled to run concurrently.
Callout: the same in-browser sandbox caveat from Concept 1 applies here, doubled — this demo’s whole point is
time.sleep()’s blocking behavior, which this particular sandbox doesn’t reliably reproduce. The output shown above is what you’d see running this code locally; treat this section as the specific case where you should mentally run the code on a real machine rather than trusting whatever the sandbox actually shows when you hit Run.
create_task() — starting something now, awaiting it later
asyncio.gather() fits the common case: start several things and wait
for all of them together, right away. Sometimes you want to start a
coroutine running in the background, do something else in the meantime,
and only collect its result later. asyncio.create_task() does exactly
that — it schedules a coroutine to start running immediately, and hands
you back a Task object you can await whenever you’re actually ready
for its result:
Notice "calling search API..." prints immediately — create_task()
starts the coroutine right away, not when it’s later await-ed.
call_search_api()’s 2-second wait is already well underway during the
0.5-second “other setup work,” so by the time await task actually runs,
much of the real waiting has already happened concurrently, in the
background. await task at the end just collects the result whenever
it’s actually needed — the task itself started running the moment
create_task() was called.
gather() and create_task() solve overlapping but distinct problems:
gather() is the right tool when you already have every coroutine you
need and just want them all run together, all at once. create_task()
fits when you want to kick something off before you’re ready to wait
for it, potentially doing unrelated work — synchronous or otherwise — in
between starting it and actually needing its result.
What happens if a coroutine's body calls the blocking time.sleep() instead of await asyncio.sleep()?