Post

async IO, coroutines & concurrency explained like cake

From Judit Polgár's chess rounds to kitchen multitasking, here's how to finally understand async IO, concurrency, threading, and multiprocessing in Python-with vivid analogies and cute code.

async IO, coroutines & concurrency explained like cake

Asynchronous Chess Exhibition

I have a question about the example in Miguel Grinberg Asynchronous Python for the Complete Beginner PyCon 2017 or Async IO in Python: A Complete Walkthrough:

“Why is it 120 * 30, based on Judit’s moves, and not something like (55 - 5) * 30 * 24 or similar-since the opponents also spend time thinking?”


TLDR

Because in the asynchronous setup, Judit is the bottleneck, not the opponents. The whole system is pipelined-parallelized-so her pacing determines the total duration. The opponents think during the time she’s playing others.


In the asynchronous version

  • Judit goes from table to table:

    • Move → move → move → … → 24 tables.
    • That takes her 24 * 5 = 120s (2 minutes per full round of 24 boards).
  • During those 120s, every opponent has time to think while she’s not at their table.

One game needs

  • 30 moves by Judit.
  • 30 moves by the opponent.

BUT! Each pair-move cycle looks like this:

  1. Judit moves on table 1.
  2. She moves on tables 2-24.
  3. Meanwhile, player 1 thinks.
  4. 120s later, she’s back to table 1.

So every opponent gets 120 seconds of think time before she returns.

And we assume earlier, opponents only need 55 seconds to move. So even if Judit takes just 120s per round, the opponents are always ready with their next move when she comes back.

They don’t delay her. So:

Total time = Judit’s total effort
= 30 moves/game * 120s/move cycle = 3600 seconds
= 1 hour


Why (55 - 5) * 30 * 24 idea doesn’t apply

(55 - 5) would imply opponents need extra time after Judit moves, but there’s no idle time between Judit moving and the opponent thinking-the wait time is already overlapped while Judit is off playing others.

So multiplying by 24 is unnecessary because the time isn’t additive, it’s overlapping.


Analogy: “Judit the Pizza Delivery Queen”

Judit is delivering pizza to 24 friends.

  • She spends 5 seconds at each house (making her move).
  • Each friend eats for 55 seconds (thinking about their move).
  • She makes the full delivery route in 120 seconds (2 mins).
  • Every time she returns to a house, the friend has finished eating and is ready again.

So she just keeps looping.

The total time is just how long she takes to cycle 30 times. Not 30×24 times.


Understand Concurrency, Parallelism, Threading, Multiprocessing

ConceptKeywordThink of it like…Best for
Concurrency“Overlapping”Juggling tasks (even if 1-at-a-time)IO-bound work
Parallelism“Simultaneous”24 bakers baking 24 cakes at onceCPU-bound work
Threading“Multiple threads in 1 brain”Switching between thoughts really fastIO-bound
Multiprocessing“Multiple brains”24 people each thinking hard at the same timeCPU-bound

Cute Kitchen Analogy

You are baking 3 cakes 🧁🧁🧁

Each one

  • Needs 10 minutes in the oven (IO-bound)
  • Needs 1 minute of intense decorating (CPU-bound)

Now let’s explore what happens in different models:


1. Threading = You have multiple hands, but still one brain (Python process)

  • You put Cake A in oven, then Cake B, then Cake C.
  • While A is baking, you decorate B. Then switch to C.
  • You never bake 2 at once, but you juggle between them without sitting idle.

Great for I/O-heavy tasks (waiting for oven = waiting for file/net/db).

Still one brain (process), just juggling faster. But in Python, GIL says “only one thread at a time runs Python bytecode.” So threading’s good only when you wait on stuff (IO).


2. Multiprocessing = You clone yourself. Now 3 yous baking 3 cakes

  • Each one has their own oven, own spatula, own brain.
  • You all bake and decorate at once.

Great for CPU-heavy stuff - like video rendering, crunching numbers, encrypting data.

Python creates separate processes (no GIL!). But heavier on memory.


3. Concurrency = Not about how many CPUs, it’s about being smart with time

  • You only have 1 brain, 1 oven. But you manage time well.
  • You bake Cake A, then while it’s in oven, switch to Cake B, then C, etc.
  • You don’t “do multiple things at the same nanosecond”, but they overlap in progress.

So concurrency is about task scheduling, not raw CPU force.


4. Parallelism = Tasks run at the exact same time on multiple CPUs

  • Like you and your twin are both decorating cakes at the same time.

Or another example: Listening to music while reading docs

Click this “Play” button, then you are doing Parallelism while reading this post.

Two totally separate things happening at the exact same time, handled by different “processors” (brain/audio center vs. brain/reading center). It’s like your OS running the music player on Core 1, and Firefox on Core 2.

Think examples like real-time simulations, machine learning model training, etc.


Python Weirdness: the GIL

Python (specifically CPython) has a GIL (Global Interpreter Lock).

This prevents multiple threads from running Python code at the same time. BUT you can do concurrent IO using threading. AND multiprocessing bypasses the GIL.

So:

Task typeBest tool in Python
Web scraping, file read/writethreading, asyncio
Heavy math, video processingmultiprocessing, concurrent.futures.ProcessPoolExecutor

In Code

Threading example

1
2
3
4
5
6
7
8
9
10
import threading

def download_file():
    print("Downloading...")

threads = []
for _ in range(10):
    t = threading.Thread(target=download_file)
    threads.append(t)
    t.start()

Multiprocessing example

1
2
3
4
5
6
7
8
9
10
from multiprocessing import Process

def crunch_data():
    print("Processing...")

procs = []
for _ in range(4):
    p = Process(target=crunch_data)
    procs.append(p)
    p.start()

async IO & coroutines

ConceptConcurrent?Parallel?Threads?Processes?
async IOYesNoNo (just 1 thread)No
coroutines(if scheduled)NoNo (just 1 thread)No

Async IO is concurrent, but not parallel. Coroutines are units of async code, and they run one at a time, but can pause for others.


What’s async IO doing?

Picture this:

You are alone in your room. You can only do one thing at a time. But you’re a super multitasker, so when you’re waiting on something (like food delivery or file download), you start doing something else immediately (like coding).

That’s async IO:

1 thread, 1 process. It pretends to be multitasking by pausing & switching tasks cleverly. Not by using threads or CPUs-but by using coroutines that can pause.


What is a coroutine?

A coroutine is like a Python function that can pause and say:

“Hey scheduler, I’m waiting for something (e.g., network reply), go let someone else run while I chill.”

So you’re not running in parallel like multiprocessing; you’re sharing a single thread - just taking turns. That’s why it’s called Cooperative multitasking - all coroutines must behave and yield when they’re waiting.


Are coroutines concurrent?

Yes, but only with async scheduling.

Coroutines are not inherently concurrent. They’re just special functions with the ability to pause. But when you await them inside an async def, and use something like asyncio.run(), then you’re scheduling them concurrently.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import asyncio

async def cook():
    print("🍳 Cooking started")
    await asyncio.sleep(2)
    print("🍳 Cooking done")

async def eat():
    print("🍽️ Eating started")
    await asyncio.sleep(1)
    print("🍽️ Eating done")

async def main():
    await asyncio.gather(cook(), eat())

asyncio.run(main())

Output

🍳 Cooking started
🍽️ Eating started
🍽️ Eating done
🍳 Cooking done

Even though both functions run on 1 thread, they interleave thanks to await.

This post is licensed under CC BY 4.0 by the author.