Concurrency vs Parallelism: What's the Difference
Basics4 minInnovialabs editorial team
In short
Concurrency is handling several tasks at once by switching between them; parallelism is literally running several tasks at the same moment on different CPU cores.
The short answer
Concurrency is about program structure: it keeps several tasks in progress and switches between them while one is waiting for something.
Parallelism is about execution: several tasks literally run at the same instant on different CPU cores.
A concurrent program can run on a single core. A parallel one needs several cores. They are not opposites: a well-built system is often both concurrent and parallel.
The kitchen analogy
Picture one cook. They put water on to boil, chop vegetables while it heats up, then stir the sauce. At any moment they do only one thing, yet three dishes move forward. That is concurrency.
Now there are three cooks, each making their own dish. Work happens at literally the same time. That is parallelism.
The key takeaway: if the cook mostly waits (water boiling, oven baking), a second cook barely helps — switching well is enough. If the cook's hands are busy the whole time (a mountain of vegetables to chop), only another pair of hands helps.
Two kinds of workload
The type of workload decides which tool fits.
- I/O-bound — the task mostly waits: a database response, an HTTP call, a file read, a message from a queue. The CPU sits idle meanwhile.
- CPU-bound — the task mostly computes: image processing, compression, encryption, calculations, model training.
I/O-bound work needs concurrency. CPU-bound work needs parallelism.
Threads, processes and async
| Mechanism | What it is | Strength | Weakness |
|---|---|---|---|
| Processes | Separate programs with their own memory | True parallelism, isolation | Heavier on memory, data sharing is harder |
| Threads | Several execution threads inside one process | Shared memory, handy for waiting on I/O | Data races, locks required |
| Async I/O | One event loop switches tasks at waiting points | Thousands of open connections at low cost | Blocking code stalls everything; needs async libraries |
Async is pure concurrency: tasks voluntarily give up control at await, and the event loop runs the next one. It is efficient as long as nobody hogs the CPU for long.
What the Python GIL is
The standard CPython implementation has a GIL (Global Interpreter Lock) — a global lock that lets only one thread execute Python bytecode at any given moment.
What this means in practice:
- Python threads work well for I/O-bound tasks: the GIL is released while waiting on the network or disk, so other threads keep going.
- Threads do not speed up CPU-bound pure Python code: computations still take turns.
- For parallel computation, use processes (
multiprocessing,ProcessPoolExecutor) or libraries that do the heavy lifting outside the GIL (many NumPy functions, for example).
Recent Python versions introduced an experimental build without the GIL, but for most projects it is still safer to assume the GIL is there.
import asyncio
from concurrent.futures import ProcessPoolExecutor
async def fetch_all(client, urls):
# I/O-bound: concurrency via async
return await asyncio.gather(*(client.get(u) for u in urls))
def heavy(n):
return sum(i * i for i in range(n))
def compute_all(numbers):
# CPU-bound: parallelism via processes
with ProcessPoolExecutor() as pool:
return list(pool.map(heavy, numbers))
How to choose
- Find out what the task waits for. If it is the network or disk, start with async or threads.
- Many connections and a modern stack (FastAPI, aiohttp, aiogram) — async.
- Legacy synchronous libraries and moderate load — a thread pool.
- Heavy computation — a process pool, or moving the work into a separate service or task queue.
- Mixed workload — async for I/O, with heavy chunks handed to a process pool via
run_in_executor.
Common mistakes
- A blocking call inside async code.
time.sleepor a synchronous database driver freezes the whole event loop. - Threads for Python number crunching in hope of a speedup — the GIL prevents it.
- Too many processes. More processes than cores usually just adds overhead.
- Shared mutable data without protection. Races show up rarely and unpredictably, which makes them hard to debug.