Concurrency in Python — The Big Picture
Why Concurrency?
Section titled “Why Concurrency?”Real programs spend a lot of time waiting for a network response, a file to load, a database query to return. During that waiting time the CPU is idle but the program is blocked, unable to do anything else. Concurrency is the set of techniques that allow a program to make progress on other work while waiting, rather than sitting idle.
Python offers three distinct models for concurrency, each designed for a different kind of problem. Choosing the wrong one not only fails to help, it can actually make things slower.
The Three Models
Section titled “The Three Models” CONCURRENCY IN PYTHON │ ┌────────────────┼────────────────┐ │ │ │ Threading Multiprocessing Async/Await │ │ │ Multiple threads, Multiple processes, Single thread, shared memory, separate memory, cooperative, I/O-bound tasks CPU-bound tasks I/O-bound tasksThreading vs. Multiprocessing
Section titled “Threading vs. Multiprocessing”Threading — One Process, Multiple Threads
Section titled “Threading — One Process, Multiple Threads”A thread is a lightweight unit of execution that lives inside a process. All threads in the same process share the same memory: they see the same variables, the same objects, the same heap:
Process (one Python interpreter, one GIL) ──────────────────────────────────────────
┌─────────────────────────────────────────┐ │ │ │ Shared Memory │ │ ───────────── │ │ variables, objects, heap │ │ ▲ ▲ ▲ │ │ │ │ │ │ │ Thread 1 Thread 2 Thread 3 │ │ (running) (waiting) (waiting) │ │ │ │ GIL — only one thread runs at a time │ └─────────────────────────────────────────┘
lightweight — spawning a thread is fast shared memory — threads can read/write the same data GIL — only one thread executes Python bytecode at a time:::danger[Threads share memory] Because threads share memory, communication between them is simple: one thread writes a value and another reads it. But this also means they can interfere with each other, requiring locks to prevent race conditions. :::
Multiprocessing — Multiple Processes, Separate Memory
Section titled “Multiprocessing — Multiple Processes, Separate Memory”A process is a completely independent Python interpreter with its own memory space, its own GIL, and its own heap. Processes do not share anything by default:
Process A Process B (own interpreter, own GIL) (own interpreter, own GIL) ────────────────────────── ──────────────────────────
┌──────────────────────┐ ┌──────────────────────┐ │ Memory A │ │ Memory B │ │ variables, heap │ │ variables, heap │ │ ▲ │ │ ▲ │ │ │ │ │ │ │ │ Thread (running) │ │ Thread (running) │ └──────────────────────┘ └──────────────────────┘ │ │ └──────────┬───────────────────┘ │ communicate via: pipes, queues, shared memory (explicit, serialised)
heavyweight — spawning a process is slow separate memory — no sharing by default no GIL contention — truly parallel on multiple cores:::danger[Processes have separate memory] Because processes have separate memory, they cannot accidentally interfere with each other. But communication requires explicit serialisation: data must be pickled, sent through a pipe or queue, and unpickled on the other side. This overhead makes multiprocessing unsuitable for tasks that require frequent communication between workers. :::
Side by Side
Section titled “Side by Side” Threading Multiprocessing ───────── ───────────────
what thread inside separate Python is created a process interpreter
memory shared separate GIL one shared GIL one GIL per process parallelism limited by GIL truly parallel overhead low high communication easy — shared memory explicit — pipes/queues risk race conditions serialisation overhead best for I/O-bound tasks CPU-bound tasksA concrete way to think about it:
Threading Multiprocessing ───────── ───────────────
like workers sharing like workers in one office separate offices │ │ they can pass notes they must send directly — fast letters — slow │ │ but only one can but all can work use the computer simultaneously at a time (GIL) on their own computer:::danger[The rule] This is why the rule is firm: for CPU-bound work, threading is not just unhelpful, it is actively worse than a single thread because of the added overhead of thread switching with no parallelism gain. :::
Threading — GIL limits parallelism ──────────────────────────────────── core 1: thread A running ──► thread A waiting for I/O GIL released ──► thread B runs core 2: idle ──► idle (GIL held by thread A or B)
only one thread runs Python at a time but I/O releases the GIL — so threading helps for I/O-bound work
Multiprocessing — no GIL contention ───────────────────────────────────── core 1: process A running ──► truly parallel core 2: process B running ──► truly parallel
each process has its own GIL — no contentionThis is why the three-model split exists:
- threading for I/O with shared memory
- multiprocessing for CPU work without GIL limits,
- asyncio for high-concurrency I/O with minimal overhead.
Implications:
- Threading does NOT speed up CPU-bound tasks (only one thread runs at a time)
- Threading DOES help I/O-bound tasks (GIL is released during I/O waits)
- Multiprocessing bypasses the GIL entirely (separate processes)