Concurrency line
Concurrency vs parallelism
A kitchen can speed up by juggling dishes or by adding burners, and the two fix different problems.
Stops explain each step. The film plays the whole ride.
- Start cooking
- Set aside
- Ready or done
- Stuck
Welcome to the kitchen
Welcome to the kitchen. Dishes wait to be cooked, a burner is where cooking happens, and a cook works one burner. The cook starts dish A.
Next stop: Dish A has to bake
All stops
The everyday version
Think of a kitchen. One cook juggling two dishes is concurrency; two cooks working side by side is parallelism.
- Dishes to cookis the Task queue
The pile of dishes waiting to be started.
- Burner 1 and its cookis the CPU core 1
Has hands for one thing at a time, but can switch dishes when one is waiting.
- Burner 2 and a second cookis the CPU core 2
An independent pair of hands. Together the two can really work at the same instant.
- The ovenis the Waiting area
A dish sits here while it bakes. Waiting needs no cook at all.
- Ready to serveis the Finished
Where finished dishes are handed out.
- One chef’s hatin a Python kitchen
You hired two cooks, but they share one chef’s hat. Only the wearer may cook, so the second cook stands idle.
Interview questions
Say the short answer first. Go deeper only if they ask.
What is the difference between concurrency and parallelism?
Concurrency is managing many tasks in progress. Parallelism is executing many at the same instant.
Concurrency is about structure and works on one core by interleaving. Parallelism needs several cores. A system can be either, or both.
“They are the same thing” or “parallel just means faster concurrent”.
Can you have concurrency on a single core?
Yes, by interleaving tasks at waits or by time-slicing.
The OS schedules threads on a core in preemptive time slices. asyncio switches cooperatively at await. Several tasks progress, but never two instruction streams at once.
“No, you need multiple cores.”
Which problems benefit from concurrency, and which from parallelism?
I/O-bound work benefits from concurrency. CPU-bound work benefits from parallelism.
Waiting tasks leave the CPU idle, so interleaving hides latency. CPU-bound tasks have no idle time, so only more cores help.
“Add threads to speed up any slow code.”
Do Python threads run in parallel?
Not for pure-Python bytecode on the default GIL build. Yes for I/O waits and GIL-releasing C code.
Only one thread holds the GIL, but blocking I/O releases it, so I/O threads overlap. Free-threaded builds, officially supported in 3.14 but optional, allow parallel bytecode.
“Python cannot do threads or parallelism at all.”
How would you speed up a CPU-bound Python function across 8 cores?
Use ProcessPoolExecutor or multiprocessing, or a library that releases the GIL.
Each process has its own interpreter and GIL. Mind pickling cost and chunk size. Subinterpreters or a free-threaded build are alternatives.
“Use ThreadPoolExecutor with 8 workers.”
How would you speed up 500 HTTP calls?
Use asyncio or a thread pool, because the calls are I/O-bound.
Latency dominates, so the waits overlap. Cap concurrency with a semaphore. Processes add heavy overhead for little gain.
“Use multiprocessing because it is faster.”
Is asyncio parallel?
No. It runs on one thread and is concurrent only.
The event loop runs one callback at a time and tasks interleave at await. Parallelism appears only if you offload work to threads or processes.
“asyncio runs tasks on multiple cores.”
Can more threads make things slower?
Yes, from contention and context switching.
Once cores or I/O are saturated, extra threads add switching cost, stack memory and lock contention. With the GIL, an I/O thread can also queue behind a CPU-bound thread each time the GIL changes hands.
“More threads is always faster.”
Cheat-sheet
Commands you will actually type.
import os; print(os.process_cpu_count())Cores this process may use (3.13+). `os.cpu_count()` is the machine total.
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor() as ex:
list(ex.map(fetch, urls))Threads for waiting-heavy work. Default workers: `min(32, cpus + 4)` on 3.13+.
from concurrent.futures import ProcessPoolExecutor
with ProcessPoolExecutor() as ex:
list(ex.map(crunch, jobs, chunksize=50))Processes for CPU-heavy work.
import asyncio
await asyncio.gather(*(fetch(u) for u in urls))Many I/O waits on one thread. Concurrent, not parallel.
import sys; sys._is_gil_enabled()3.13+: is the GIL on right now? Tells you if threads can run bytecode in parallel.
python -VVMentions “free-threading build” when you run one.
import time
t = time.perf_counter()
work()
print(time.perf_counter() - t)Measure wall-clock before and after. Never assume a speed-up.