The Secret Life of Python: Never Block the Asyncio Loop
The Secret Life of Python: Never Block the Asyncio Loop
Why CPU-bound code freezes your server and how to fix it with to_thread
#PythonAsync #EventLoop #BlockingCode #ResponsiveCode
Margaret is a senior software engineer. Timothy is her junior colleague. They work in a grand Victorian library in London — the kind of place where code quality is the unspoken objective, and craftsmanship is the only thing that matters.
Episode 41
Timothy was feeling invincible. He had successfully juggled Alice, Bob, and Charlie using asyncio.gather(). But then, he decided to add one more feature to his ranking request: a Security Hash.
"It’s just a small calculation," Timothy told Margaret. "Before I return the rank, I'll run a heavy for loop to generate a secure token. It shouldn't take more than two seconds. Since it's inside an async function, the Juggler will just handle it while he waits for the others, right?"
He added the "Selfish Loop" to Alice's request.
import asyncio
import time
async def check_ranking(player_name, is_selfish=False):
print(f"Juggler: Starting request for {player_name}...")
if is_selfish:
# THE TRAP: A heavy CPU task that doesn't use 'await'
print(f"--- {player_name} is being selfish! Starting heavy math... ---")
# This is a CPU-bound task. It doesn't throw a ball in the air;
# it forces the Juggler to stand still and calculate.
sum(i * i for i in range(10_000_000))
print(f"--- {player_name} finished the math. ---")
await asyncio.sleep(1)
print(f"Juggler: Finished request for {player_name}!")
async def main():
print("--- The Juggler is starting the show ---")
# Alice is going to be the 'Selfish' one
await asyncio.gather(
check_ranking("Alice", is_selfish=True),
check_ranking("Bob"),
check_ranking("Charlie")
)
if __name__ == "__main__":
asyncio.run(main())
Timothy hit enter. Usually, Bob and Charlie would start their requests immediately after Alice. But this time, the screen stayed dead.
Alice started her math. Two seconds passed. Only after Alice finished her math did Bob and Charlie’s names even appear on the screen.
"Margaret!" Timothy cried. "Alice froze the whole stage! Bob and Charlie were just standing there waiting for her to finish her math. Why didn't the Juggler throw her ball in the air?"
The Juggler’s Hands are Full
Margaret walked over to the whiteboard. "Remember the Juggler's hands, Timothy. The Juggler can only throw a ball in the air when they reach an await point."
"In your code," she explained, "Alice started doing heavy math. That’s not 'waiting'—that’s 'working.' The Juggler had to hold that ball with both hands and focus entirely on the calculation. Because the Juggler was busy, they couldn't move their hands to pick up Bob or Charlie’s balls."
"This is the Cardinal Sin of Asyncio," Margaret said. "If you block the thread with heavy work, the Event Loop stops for everyone. One selfish player just crashed the experience for your other 4,999 users."
The Specialist’s Solution: Offloading
"So I can't do math in Asyncio?" Timothy asked, defeated.
"You can," Margaret said, "but you have to send that ball to a different stage. If you have heavy work, you use a Thread or a Process to do the heavy lifting, and you await the result. In modern Python, we use to_thread to do this easily."
# The correct way to offload heavy math so the Juggler stays free
def heavy_math():
return sum(i * i for i in range(10_000_000))
# Inside your async function:
result = await asyncio.to_thread(heavy_math)
"That way," Margaret said, "the Juggler’s hands stay free to keep the other 4,999 balls moving."
Margaret’s Cheat Sheet: The Rules of the Loop
The Cardinal Rule
- Never Block the Loop: Any code that takes a long time to run without an
await(like a massiveforloop) will freeze the entire program for every user.
Detect the Trap
- Does it do math? → CPU-bound → Offload it with
to_thread. - Does it wait for a database? → I/O-bound → Use
await. - Does it use
time.sleep()? → STOP. This is "Selfish." Always useawait asyncio.sleep().
The Escape Hatch
asyncio.to_thread(): Added in Python 3.9, this is the easiest way to run a blocking function in a separate thread without freezing the Juggler.
Aaron Rose is a software engineer and technology writer at tech-reader.blog.
Catch up on the latest explainer videos, podcasts, and industry discussions below.


Comments
Post a Comment