2 Concurrency
Modern software often needs to handle multiple tasks at once to stay responsive, efficient, and scalable, but doing so introduces complexity around timing, coordination, and shared resources. The chapter explains that concurrency is about structuring work so tasks can be managed independently, while parallelism is about executing multiple tasks simultaneously. Using the coffee shop analogy, it shows that adding more workers and machines creates parallelism, while reorganizing the workflow into separate order-taking and coffee-making stages creates concurrency. The two ideas are distinct, but they can complement each other: concurrency can provide the structure that makes some parts of a system easier to parallelize.
The chapter then introduces coroutines as a lightweight way to achieve concurrency without parallelism. Unlike ordinary functions that run from start to finish, coroutines can pause and resume execution, preserving their state between steps. Because they run within a single thread, they avoid many complications of true parallel execution while still allowing programs to switch between tasks efficiently. This makes them useful for asynchronous I/O, data pipelines, generators, and other situations where work often involves waiting rather than constant CPU computation.
For cases where multiple threads access shared resources, the chapter explains synchronization tools such as mutexes and semaphores. A mutex protects a single resource or critical section by allowing only one thread to access it at a time, while a semaphore manages access to a limited pool of resources using a count. The chapter closes by distinguishing data races from race conditions: a data race happens when threads access the same memory concurrently and at least one access is a write, while a race condition is any timing-dependent behavior that affects the result. Synchronization can eliminate data races, but it does not automatically make a program deterministic, so developers must identify which kind of concurrency problem they are actually solving.
Summary
- Concurrency is about structure; parallelism is about execution. Concurrency organizes tasks to be handled independently (like a single waiter switching tasks), whereas parallelism executes multiple tasks simultaneously by adding resources (like two waiters working at once).
- Coroutines enable cooperative concurrency. Unlike standard functions that run to completion, coroutines can pause (yield) and resume execution within a single thread, making them ideal for I/O-bound tasks without the overhead of multi-threading.
- Mutexes and Semaphores manage resource access differently. A Mutex allows only one thread to access a resource at a time (ownership), while a Semaphore allows multiple threads up to a set limit (signaling).
- Data Races differ from Race Conditions. A data race is a specific memory error where unsynchronized threads access a variable simultaneously. A race condition is a broader flaw where the system's output depends on uncontrollable timing events
FAQ
What is the main difference between concurrency and parallelism?
Concurrency is about structure: organizing work so multiple tasks can be managed independently. Parallelism is about execution: actually running multiple tasks at the same time, usually by using multiple threads, cores, or machines.
Can a program be concurrent without being parallel?
Yes. A program can be concurrent without running tasks simultaneously. For example, coroutines can pause one task while waiting for I/O and resume another task on the same thread. The tasks are structured independently, but only one is executing at any given moment.
Can parallelism exist without concurrency?
Yes. Parallelism means multiple tasks are executing at the same time, but those tasks do not necessarily have to be structured as independently coordinated parts of one workflow. For example, processing several independent user requests at the same time is parallel execution, but each request may still be handled as a simple sequential task.
How does the coffee shop example explain concurrency?
In the coffee shop analogy, concurrency appears when the work is restructured into independent stages: one waiter takes orders, another makes coffee, and customers move to a separate queue while waiting. The key change is not that everything happens at the same time, but that the workflow is organized so different parts can proceed independently.
How does the coffee shop example explain parallelism?
Parallelism appears when the coffee shop adds more resources, such as a second waiter and a second coffee machine, so two customers can be served at the same time. This improves throughput by executing multiple tasks simultaneously.
What are coroutines?
Coroutines are special functions that can pause and resume their execution. Unlike regular functions, which run from start to finish, a coroutine can yield control, save its current state, and continue later from where it left off.
Why do coroutines provide concurrency but not necessarily parallelism?
Coroutines can manage multiple tasks within a single thread by switching between them. This gives the program a concurrent structure, but because everything still runs on one thread, the tasks are not executing at the same time. Therefore, coroutines provide concurrency without requiring parallelism.
What is the difference between a mutex and a semaphore?
A mutex allows only one thread to access a specific resource or critical section at a time. It is a lock with ownership: the thread that locks it should unlock it. A semaphore controls access to a limited pool of resources using a count. Multiple threads may proceed as long as the count indicates that resources are available.
When should I use a mutex instead of a semaphore?
Use a mutex when you need mutual exclusion for a single shared resource or critical section, such as protecting a shared variable from simultaneous access. Use a semaphore when you need to limit access to a pool of resources, such as a fixed number of database connections, worker slots, or parking-space-like capacity.
What is the difference between a data race and a race condition?
A data race occurs when two or more threads access the same memory location at the same time and at least one access is a write. A race condition is broader: it happens when the program’s result depends on uncontrollable timing, such as which thread runs first. All data races are race conditions, but not all race conditions are data races.
The Coder Cafe ebook for free