Concurrency and Parallelism
Concurrency means dealing with several tasks at once by switching between them so all make progress. Parallelism means actually running several tasks at the same instant on different CPU cores.
The difference
- Concurrent: one cook switching between three dishes, never idle while one waits for water to boil.
- Parallel: three cooks, each cooking a dish at the same time.
A program can be concurrent on a single core and parallel only with several cores. Concurrency is about structure, parallelism about execution.
Why use it
- Waiting-heavy work (I/O-bound) such as network calls, disk and databases: while one task waits, another runs.
- Computation-heavy work (CPU-bound) such as image processing: real speed-up needs several cores.
Common tools
| Tool | Idea |
|---|---|
| Threads | Several paths of execution in one process, sharing memory |
| Processes | Separate programs with separate memory: safer, heavier |
| Async / await | While one task waits for I/O, the same thread runs others |
| Worker pools and queues | A set of workers takes jobs from a queue |
Languages differ in the details; check your language’s documentation.
# Python: two one-second waits finish in about one second, not two
import asyncio
async def main():
await asyncio.gather(asyncio.sleep(1), asyncio.sleep(1))
asyncio.run(main())
What goes wrong
- A race condition means the result depends on which task runs first. Two tasks both read a counter, add one and write it back, and one update is lost.
- A deadlock means two tasks each wait for something the other holds, so neither moves.
- Shared changeable state is the root of most of these. See State Management.
- The bugs appear only sometimes, and logging or a debugger can change the timing, so they are hard to reproduce.
Reducing the risk
- Avoid sharing state; pass messages or immutable data.
- If state must be shared, protect it with locks, thread-safe structures or database transactions.
- Prefer queues, async and pools over raw threads, and add concurrency only after measuring a need (Performance Basics).
Try it
Run the snippet above, then replace one of the two tasks with an async def function that calls the blocking time.sleep(1). How does the total time change, and why?
Common mistakes
- Updating shared state from several tasks without protection, so updates are lost.
- Calling blocking code such as time.sleep inside async code.
- Adding threads before measuring whether they are needed.
- Debugging timing bugs with logging or a debugger that changes the timing.
Check yourself
- What is the difference between concurrency and parallelism?
- How can two tasks incrementing a counter lose an update?
- Name two ways to reduce the risk of shared state.