Skip to content
Innovialabs

Concurrency vs Parallelism: What's the Difference

Basics4 min

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

MechanismWhat it isStrengthWeakness
ProcessesSeparate programs with their own memoryTrue parallelism, isolationHeavier on memory, data sharing is harder
ThreadsSeveral execution threads inside one processShared memory, handy for waiting on I/OData races, locks required
Async I/OOne event loop switches tasks at waiting pointsThousands of open connections at low costBlocking 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

  1. Find out what the task waits for. If it is the network or disk, start with async or threads.
  2. Many connections and a modern stack (FastAPI, aiohttp, aiogram) — async.
  3. Legacy synchronous libraries and moderate load — a thread pool.
  4. Heavy computation — a process pool, or moving the work into a separate service or task queue.
  5. 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.sleep or 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.

FAQ

Yes. An async server on one core serves thousands of connections by switching between them, yet runs only one task at any instant.

We can help with this

If you'd rather get the result than figure it all out yourself — this is what we do every day.

Discuss a project

Read next

Have an idea or a task? Tell us about it

We'll reply within one business day and propose a solution. The consultation is free.