Command Palette

Search for a command to run...

Hectal
PHASE 12Advanced ~28 min· topic 1 of 9

Topic 12.1

Concurrency vs Parallelism: Threads, Processes, Async

In one line

Concurrency means several tasks are in progress at once, even if only one runs at any instant; parallelism means several really run at the same instant. Python gives you three tools (threads, processes and asyncio), and the right one depends on whether your program mostly waits (I/O-bound) or mostly calculates (CPU-bound).

Think of it like this

One cook in a kitchen. She puts rice on the stove, and while it cooks she chops vegetables, and while the oven heats she washes the pans. She never does two things with her hands at the same instant, yet three dishes are in progress together because she does useful work during the waiting. That is concurrency. Now add a second cook with a second stove: two things really happen at the same instant. That is parallelism.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Concurrency
Several tasks are in progress at the same time, though maybe only one runs at any instant.
Parallelism
Several tasks really run at the same instant on different CPU cores.
I/O-bound
Spends most of its time waiting for the network, a disk or a database.
CPU-bound
Spends most of its time calculating, so the CPU is the bottleneck.
Thread
A flow of code inside a process. Threads in one process share the same memory.
Process
A running program with its own memory. Processes do not share variables.
Event loop
The asyncio loop that runs tasks and resumes each one when what it awaited is ready.
LLM API
A web service you send text to and get a language model's answer from. The call mostly waits for the reply.
Coroutine
A function made with async def. It can pause at an await and be resumed later.
Cooperative switching
A task keeps running until it chooses to wait (at an await).
Preemptive switching
The operating system pauses a thread at any moment to let another run.

Step by step

01Waiting is the thing to overlap

Run a slow job twice, one after the other, and the total is the sum of both waits. If the waits can overlap, the total is only the longest wait. Nothing got faster: the program just stopped wasting the waiting time.

Waiting is the thing to overlapdiagram
Rendering diagram…

02The same two jobs in code

Below, boil is a coroutine: a function written with async def that can pause and let others run. Topic 12.6 teaches async and await properly. For now read await asyncio.sleep(...) as "wait here without blocking anyone else". It stands in for waiting on a network.

Awaiting the jobs one by one runs them in order. asyncio.gather starts both and waits for both, so the short job finishes first.

Look at the order of the printed lines, not at any clock. In the second half both start lines come first, because the kettle gives way while it waits and the toaster gets its turn.

main.pywhole filepython
async def main():
    results = await asyncio.gather(boil("kettle", 0.1), boil("toaster", 0.05))
    print(results)

03CPU-bound work cannot be overlapped by waiting

If a task never waits, there is no waiting time to reuse. Adding up ten million numbers in pure Python keeps one core 100% busy. Splitting it across threads does not add cores for Python code, and asyncio cannot help at all, because there is no await to switch at.

To use more cores you need more processes (Topic 12.5), or a library whose heavy loop is written in C (NumPy), or the free-threaded build (Topic 12.2).

04The three models side by side

Each model answers a different question: threads and asyncio overlap waiting; processes add cores. Many real systems mix them: an async web server that hands heavy maths to a process pool.

The three models side by sidediagram
Rendering diagram…

05Cooperative vs preemptive, in one picture

With threads, the operating system can stop a thread between any two steps, so two threads can interleave inside one line like x += 1 (Topic 12.3 shows exactly how this breaks). With asyncio, switching happens only at await, so between two awaits your code runs alone. That single rule removes a whole class of bugs, and creates one new one: code that blocks, such as time.sleep(1), blocks every task.

06What to expect from this phase

Topics 12.2 to 12.5 cover the thread and process side; Topics 12.6 to 12.8 cover asyncio in depth; Topic 12.9 is the decision guide. You will meet this again in Phase 16, where async LLM clients, retries and streaming responses are built from exactly these pieces.

On this page the Run button runs Python inside your browser. The browser sandbox cannot start threads or processes, so those topics mark which examples are live and which were run on a normal Python.

Try it yourself

  1. 1

    Make the waits longer

    In the first example change the kettle's wait to 0.2 and the toaster's to 0.01. Predict which done line comes first in the "together" half, then run it. The order depends only on the waits, never on the order you wrote the calls.

  2. 2

    Add a third job

    Add a third boil("oven", 0.07) to the gather call. Where does its done line appear? How many start lines come before the first done?

  3. 3

    Classify your own programs

    In the second example add three jobs you know from your own life of coding (downloading a file, sorting a million numbers, asking a chatbot a question) and decide the three flags for each. If you cannot decide, ask: "if I gave this a faster internet connection, would it speed up? If I gave it a faster CPU?"

Code & diagrams

Two waits: one after another, then together New tab

Watch the order of the lines. In the second half both jobs start before either finishes.

Sign in to run this example in your browser.

Expected output

--- one after another ---
kettle: start
kettle: done
toaster: start
toaster: done
--- together ---
kettle: start
toaster: start
toaster: done
kettle: done
results: ['kettle', 'toaster']
A decision helper for the three models New tab

A toy version of the decision table. Change a job's flags and predict the answer first.

Sign in to run this example in your browser.

Expected output

1000 LLM API calls                     -> asyncio
resize 500 photos with pure Python     -> processes (or NumPy)
read 20 files                          -> threads (or asyncio)
add up a list of 100 numbers           -> plain code: no concurrency needed
Decision tablediagram
Rendering diagram…
Loading a dashboard from three services at once New tab

A realistic shape: one page needs data from several services. The sleeps stand in for network replies. Read the order of the lines; the page is ready when the slowest service answers.

Sign in to run this example in your browser.

Expected output

weather: request sent
orders: request sent
prices: request sent
prices: reply received
weather: reply received
orders: reply received
sections: ['orders', 'prices', 'weather']
orders: {'open': 4, 'late': 1}

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Block the loop with time.sleep

In boil, replace await asyncio.sleep(seconds) with time.sleep(seconds) (and add import time). The program is still correct, but look at the order.

main.pywhole filepython
import asyncio
import time


async def boil(name, seconds):
    print(f"{name}: start")
    time.sleep(seconds)            # blocking sleep: nobody else can run meanwhile
    print(f"{name}: done")


async def main():
    await asyncio.gather(boil("kettle", 0.1), boil("toaster", 0.05))


asyncio.run(main())
terminal
$ python main.py
── what you'll see ──
kettle: start
kettle: done
toaster: start
toaster: done

Myth vs fact

Myth

Concurrency and parallelism are the same thing.

Fact

Concurrency is about structure (many tasks in progress); parallelism is about execution (many running at the same instant). asyncio is concurrent but not parallel.

Myth

More threads always make a program faster.

Fact

Only if the program spends time waiting. For pure-Python calculation, extra threads add switching cost and no speed (Topic 12.2).

Myth

asyncio makes code run on several cores.

Fact

asyncio uses exactly one thread, so one core. It wins by using the waiting time, not by adding cores.

Myth

Async is only for web servers.

Fact

Any program that waits on many things can use it: scripts that call APIs, scrapers, data loaders, agents that call tools.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Rule of thumb for the cost per unit: a coroutine costs a few kilobytes and is created in microseconds, so tens of thousands are normal. A thread costs a stack (megabytes of address space) and an operating-system object, so hundreds are normal. A process costs a whole interpreter, so you create about as many as you have cores.

  • ▸

    Real systems stack the models: an async server for the many waiting requests, a thread pool (asyncio.to_thread) for the one blocking library, a process pool for heavy maths, and native libraries underneath. Choosing is per bottleneck, not per application.

  • ▸

    Concurrency adds non-determinism: the order of events can change from run to run, which is why this phase's examples print order, not timings, and why concurrent code needs deliberate tests (Topic 12.9).

  • ▸

    A useful sizing rule is Little's law: the number of calls in flight is roughly the request rate multiplied by the latency of one call. To sustain 50 requests per second against an API that takes 0.8 s per call you need about 40 calls in flight. That number, not the core count, sets the size of a semaphore or a thread pool for I/O-bound work.

  • ▸

    asyncio removes races between awaits, not races across them. A check-then-act sequence with an await in the middle (if key not in cache: cache[key] = await load(key)) can still run twice, because another task runs during the await. Topic 12.7 shows an asyncio.Lock for exactly this.

Remember this

  1. 1

    A program is I/O-bound when most of its time is spent waiting for something outside the CPU: a network reply, a disk read, a database answer, an AI model generating text. It is CPU-bound when most of its time is spent calculating: resizing images, training a model in pure Python, hashing files. This one question decides almost everything in this phase, so ask it first about any slow program.

  2. 2

    Concurrency helps I/O-bound programs. While one request waits for a reply, another can be sent. You do not need more CPU for that, because the CPU was idle anyway. Calling an LLM API takes about a second of waiting for a few milliseconds of your own work, so sending 100 calls together finishes in about the time of one call instead of 100.

  3. 3

    Parallelism helps CPU-bound programs, but only if the work runs on several CPU cores at the same time. Python's threads cannot do this for pure-Python code (the GIL, Topic 12.2); separate processes can, because each process has its own interpreter and its own memory. Libraries like NumPy also run in parallel inside their C code. That is why vectorised maths (one command that works on a whole array of numbers at once, Topic 14.3) is often the best answer.

  4. 4

    Python has three tools. Threads (threading, Topic 12.3) run several flows of code inside one process, sharing memory; the operating system switches between them at any moment. Processes (multiprocessing, Topic 12.5) are separate programs with separate memory; they use all cores but cost more and must send data by copying. asyncio (Topics 12.6 to 12.8) runs many tasks on one thread, and a task only gives way at an await, a point where it says "I am waiting, someone else can go".

  5. 5

    The three tools differ in who decides when to switch. Threads use preemptive switching: the operating system can interrupt a thread anywhere, even in the middle of counter += 1, which is why threads need locks. asyncio uses cooperative switching: a task is never interrupted, it hands control back only at await, which is why asyncio needs far fewer locks but one task that never awaits freezes everything.

  6. 6

    In the AI era most Python concurrency is I/O-bound: many model calls, many web pages, many database rows. That is why asyncio is the default for LLM applications, and why it is the longest part of this phase. CPU-bound work (embeddings, matrix maths) usually runs in NumPy, PyTorch or another native library that already uses many cores for you.

Explain it without notes

01

What is the difference between concurrency and parallelism? Which does asyncio give you?

02

What is the difference between I/O-bound and CPU-bound work? Give one AI-related example of each.

03

Why is asyncio called cooperative, and what is the danger of that?

04

A script calls an LLM API 500 times, one after another, and takes ten minutes. What would you change, and why does it work?

Practice

01

Classify each as I/O-bound or CPU-bound: (a) downloading 50 PDFs, (b) computing the 35th Fibonacci number with recursion, (c) reading 10,000 rows from a database over the network.

02

Using asyncio.gather, run three coroutines a, b, c that sleep 0.03, 0.01 and 0.02 seconds, and print their names in the order they finish.

03

Write a function choose(job) that returns "asyncio" when job is "many api calls", "processes" when it is "heavy maths", and "no concurrency" otherwise.

Trade-offs

  • ↔

    Threads: easy to start and work with blocking libraries, but they share memory, so you need locks, and they do not speed up pure-Python calculation.

  • ↔

    Processes: real parallelism and strong isolation, but heavy to start, and every argument and result must be copied between them.

  • ↔

    asyncio: very cheap tasks and few race conditions, but every library in the chain must be async-aware, and one blocking call stalls everything.

Done when you can

  • Done when you can explain concurrency vs parallelism with the kitchen analogy.

  • Done when you can tell whether a program is I/O-bound or CPU-bound with the faster-network or faster-CPU question.

  • Done when you can name the three Python models and say which kind of work each one helps.

  • Done when you can read the printed order of two overlapping coroutines and say why it came out that way.

  • Done when you can explain why time.sleep inside a coroutine defeats gather.