Concurrency Fundamentals
Concurrency versus parallelism, processes versus threads versus async, and the bugs shared state causes: a lost update and a deadlock reproduced on purpose, then fixed with a lock and a lock order.
Overview
Concurrency means a program has several tasks in progress during the same stretch of time. It does not mean they run at the same instant; that is parallelism. Almost every backend is concurrent: a server handles many requests at once, a worker runs background jobs, a cache is read and written from several threads.
Interviewers ask about it because the bugs are subtle. Code that looks right on one thread can lose updates, hang forever, or behave differently on every run once two threads share data. A strong candidate names the failure, shows the interleaving that causes it, and fixes it with the smallest tool that works.
Concurrency is not parallelism
Rob Pike's short version: concurrency is dealing with many things at once, parallelism is doing many things at once. Concurrency is how you structure the program; parallelism is how it happens to run on the hardware.
- 1 — task A — core 1: task A, core 2: task B
- 2 — task B — core 1: task A, core 2: task B
- 3 — task A — core 1: task A finishes, core 2: task B
- 4 — task B — core 2: task B finishes
On one core the tasks take turns, and the switching makes them look simultaneous. On two cores they really overlap. The same program can run either way: in Go, the GOMAXPROCS setting limits how many OS threads run Go code at the same time; in CPython, the global interpreter lock (GIL) lets only one thread run Python bytecode at a time, so threads give concurrency but not CPU parallelism (the optional free-threaded build of Python 3.13 and later removes that limit).
Processes, threads and async
A process has its own memory; the operating system keeps processes apart. A thread runs inside a process and shares that process's memory with its sibling threads. An event loop (async) runs many tasks on one thread, switching only at points where a task waits for I/O.
- Memory — Separate; talk through pipes, sockets or shared memory — Shared; needs locks — Shared, but only one task runs at a time
- Cost to create — Highest — Lower; each has its own stack — Lowest; a task is an object
- CPU parallelism — Yes — Yes in Java, Go, C++; not for Python bytecode under the GIL — No; one thread
- A crash affects — Only that process — The whole process — The whole loop
- Fits — CPU-heavy work, isolation — Shared state, blocking libraries — Many slow I/O waits (sockets, HTTP calls)
The Python docs say it directly: to use several cores for CPU work, use multiprocessing or ProcessPoolExecutor; threads remain a good fit for I/O-bound work, because the GIL is released while a thread waits on I/O.
The async model in action: ten tasks that each wait 50 ms finish together in about 50 ms, on one thread, because each await hands the thread to another task.
Race conditions
A race condition is a bug where the result depends on how threads happen to interleave. The classic one is the lost update: count += 1 is really read, add, write, and two threads can both read the old value.
- 1 — reads 0 — 0
- 2 — reads 0 — 0
- 3 — writes 1 — 1
- 4 — writes 1 — 1 (one increment lost)
The code below forces exactly that interleaving with a barrier, so the bug shows every run instead of once in a million. The section of code that touches shared state is the critical section; putting it under a lock makes the read and the write one step, and the update is no longer lost.
Deadlock, livelock and starvation
A deadlock is a set of threads that each wait for something another one holds, so none can move. Coffman, Elphick and Shoshani (1971) listed four conditions that must all hold for a deadlock; remove any one and it cannot happen.
- Mutual exclusion — A resource is held by one thread at a time — Share it (read-only data, lock-free structures)
- Hold and wait — A thread holds one resource while waiting for another — Acquire everything at once, or nothing
- No preemption — A held resource cannot be taken away — tryLock with a timeout; on failure release what you hold and retry
- Circular wait — Threads wait in a cycle — A global lock order: everyone takes locks in the same order
There are two related failures. In a livelock, threads keep reacting to each other and changing state but none finishes, like two people stepping aside in a corridor in the same direction again and again; random backoff fixes it. In starvation, a thread is ready to run but never gets the resource because others always win it; a fair lock or a queue fixes it.
The program below builds the deadlock on purpose: two threads take the same two locks in opposite orders. It uses a timeout so it reports the deadlock instead of hanging. Then every thread takes the locks in one global order (by id) and both finish.
What you will be able to do
- Explain concurrency versus parallelism with a timeline, and say what the GIL and GOMAXPROCS change.
- Choose between processes, threads and async for a workload, with the reason.
- Show a lost update as an interleaving and fix it with a lock around the critical section.
- Name the four Coffman conditions and break the one that fits: usually circular wait, with a lock order.
- Tell deadlock, livelock and starvation apart.
Interview questions
- What is the difference between concurrency and parallelism?
- What are the four conditions for deadlock, and how do you prevent it?
- When should you use processes, threads or async?
- Show a race condition and fix it.
- What is the difference between deadlock, livelock and starvation?
Sign up free to read the full topic
More Low-Level Design topics
Start free on Cariara · Coding problems · System design studio