In the previous chapter, we looked at how the operating system decides which process or thread gets access to the CPU at any given moment, using scheduling algorithms like FCFS, SJF, and Round Robin. What we didn't address is what happens when several processes or threads, running concurrently, need to access and modify the very same piece of shared data at roughly the same time. Without careful coordination, this situation can quietly produce incorrect results, even though every individual process appears to be working correctly on its own. This is exactly the problem that process synchronization is designed to solve.
Process synchronization becomes especially important because of concepts covered earlier in this series. Threads within the same process already share memory directly, as explained in the Threads chapter, and even separate processes can share data through mechanisms like shared memory, covered under Interprocess Communication in the System Calls chapter. Whenever data is shared this way, the operating system needs reliable tools to make sure that shared access doesn't lead to corrupted or inconsistent results.
| Step | Question to Ask | Concept |
|---|---|---|
| 1 | What data or resource is shared? | Shared Resource |
| 2 | What can go wrong if operations overlap? | Race Condition |
| 3 | Which exact code must be protected? | Critical Section |
| 4 | How do we allow safe access? | Mutex / Semaphore / Monitor |
| 5 | Does the solution remain fair and deadlock-free? | Progress / Bounded Waiting / Deadlock |
A race condition occurs when two or more processes or threads access shared data at the same time, and the final outcome depends on the unpredictable order in which their individual operations happen to execute. Because the exact timing of context switches, covered in the Process Management chapter, cannot generally be predicted in advance, a race condition can produce different, incorrect results on different runs, even though the underlying code never changes.
A classic example involves two processes both trying to increase the value of a shared counter variable by one. Incrementing a variable might look like a single, instant operation in code, but at the hardware level it actually involves three separate steps: reading the current value, adding one to it, and writing the new value back. If two processes interleave these steps — for example, both reading the same original value before either one writes its updated result back — one of the increments can be completely lost, leaving the counter with a smaller final value than expected, even though both processes appeared to run successfully.
shared counter = 5 Thread T1: counter = counter + 1 Thread T2: counter = counter + 1 Conceptual steps: T1: read 5 → calculate 6 T2: read 5 → calculate 6 T1: write 6 T2: write 6 Expected after two increments: 7 Possible result after unsafe interleaving: 6
The specific part of a program's code where a process accesses shared data that could be affected by a race condition is called the critical section. The critical section problem refers to the challenge of designing a way for multiple processes to cooperate so that, when one process is executing inside its critical section, no other process is allowed to enter its own critical section at the same time, since doing so could lead to exactly the kind of race condition described above.
A correct solution to the critical section problem must satisfy three essential requirements. The first is mutual exclusion, meaning if one process is currently executing inside its critical section, no other process is permitted to enter its own critical section at the same time. The second is progress, meaning if no process is currently in its critical section, and one or more processes want to enter, the decision about which one enters next cannot be postponed indefinitely. The third is bounded waiting, meaning there must be a limit on how many times other processes are allowed to enter their critical section before a process that is waiting is finally given its turn, preventing that process from waiting forever.
Process P: while true: // Entry section request permission // Critical section update shared resource // Exit section release permission // Remainder section perform independent work
A mutex, short for mutual exclusion, is one of the simplest tools used to solve the critical section problem. It works like a lock with only two possible states: locked or unlocked. Before a process enters its critical section, it must first acquire the mutex, which locks it. If another process attempts to acquire the same mutex while it is already locked, that process is forced to wait until the mutex is released. Once the first process finishes its work inside the critical section, it releases the mutex, unlocking it and allowing exactly one waiting process to acquire it next.
Because a mutex can only be in one of two states, it is particularly well suited to situations where exactly one process at a time should be allowed access to a single shared resource, such as a shared variable or a single shared file.
mutex M = UNLOCKED acquire(M) // Critical section update shared data release(M) // Continue independent work
A semaphore is a more general synchronization tool than a mutex, built around an integer variable that can
only be accessed through two specific, carefully controlled operations, traditionally called
wait() and signal(), and sometimes referred to as P() and
V(). The wait() operation decreases the semaphore's value, and if the resulting
value becomes negative, the calling process is blocked until the value becomes non-negative again. The
signal() operation increases the semaphore's value and, if there are any processes currently
blocked and waiting, allows one of them to proceed.
Semaphores are generally divided into two types. A binary semaphore can only hold the value 0 or 1, and behaves very similarly to a mutex, allowing only one process into a critical section at a time. A counting semaphore can hold any non-negative integer value, and is used to manage a resource that has multiple identical instances available, such as a pool of several identical printers, where the semaphore's value represents the number of resource instances currently available.
wait(S): perform atomic wait operation if no permit is available: block until permitted otherwise: consume one permit signal(S): perform atomic signal operation return one permit // Example use: wait(S) use shared resource signal(S)
| Mutex | Semaphore |
|---|---|
| Simple locked/unlocked mechanism | Integer-valued mechanism, controlled through wait() and signal() |
| Typically owned and released by the same process that locked it | Can be signalled by a different process than the one that waited on it |
| Best suited for protecting a single shared resource | Best suited for managing multiple instances of a resource, or coordinating events between processes |
While semaphores are powerful, they place the entire responsibility of using wait() and
signal() correctly on the programmer, and a single misplaced or forgotten call can silently
introduce a race condition or cause a permanent deadlock. A monitor addresses this concern by providing a
higher-level construct that bundles shared data together with the procedures that are allowed to operate on
that data, all within a single unit.
Inside a monitor, only one process is allowed to be active at a time, similar to the mutual exclusion guarantee a mutex provides, but this exclusion is enforced automatically by the monitor itself rather than requiring the programmer to manually acquire and release a lock at every access point. Monitors also provide condition variables, which allow a process to wait inside the monitor until a particular condition becomes true, and to signal other waiting processes once that condition has been satisfied, offering a cleaner and less error-prone way to coordinate complex synchronization scenarios compared to using raw semaphores directly.
Several well-known problems are commonly used to illustrate synchronization challenges and to demonstrate how tools like semaphores and monitors can be applied to solve them. These problems appear frequently in exams and interviews, so understanding the core challenge behind each one is valuable.
The Producer-Consumer problem, also called the Bounded Buffer problem, involves two types of processes sharing a fixed-size buffer: producers, which generate data and place it into the buffer, and consumers, which remove data from the buffer to process it. The core challenge is making sure a producer never tries to add an item to a buffer that is already completely full, and a consumer never tries to remove an item from a buffer that is currently empty, while also making sure the buffer itself is never accessed by a producer and a consumer at exactly the same time. This problem is typically solved using a combination of counting semaphores, one tracking the number of filled slots and another tracking the number of empty slots, along with a mutex protecting the buffer itself during each individual insertion or removal.
semaphore empty = BUFFER_SIZE semaphore full = 0 mutex bufferLock Producer: item = produce() wait(empty) acquire(bufferLock) insert item into buffer release(bufferLock) signal(full) Consumer: wait(full) acquire(bufferLock) item = remove from buffer release(bufferLock) signal(empty) consume(item)
The Readers-Writers problem involves a shared resource, such as a piece of data or a file, accessed by two types of processes: readers, which only read the data without modifying it, and writers, which modify the data. The synchronization challenge here is subtle: multiple readers can safely access the shared data at the same time without any risk of corrupting it, since none of them are changing anything, but a writer requires exclusive access, meaning no reader or other writer can be accessing the data while a writer is modifying it. Solutions to this problem generally use semaphores to track how many readers are currently active, only requesting exclusive access on behalf of all the readers when the very first reader arrives, and releasing that exclusive access only once the very last reader finishes.
Reader: acquire(readerCountLock) readerCount++ if readerCount == 1: acquire(resourceLock) release(readerCountLock) read shared data acquire(readerCountLock) readerCount-- if readerCount == 0: release(resourceLock) release(readerCountLock) Writer: acquire(resourceLock) modify shared data release(resourceLock)
Readers may share access under the selected policy; a writer requires exclusive access.
The Dining Philosophers problem imagines a group of philosophers sitting around a circular table, alternating between thinking and eating, with exactly one fork placed between each pair of adjacent philosophers. To eat, a philosopher needs to pick up both the fork on their left and the fork on their right, but since forks are shared between neighbouring philosophers, only one of any two neighbours can be holding their shared fork at a time. This problem is famous specifically because a naive solution, where every philosopher simply picks up their left fork first and then waits for their right fork, can result in a deadlock if every philosopher picks up their left fork at exactly the same moment, leaving all of them waiting forever for a right fork that will never become available. Common solutions include allowing only a limited number of philosophers to attempt picking up forks at once, or requiring philosophers to pick up their forks in a specific, consistent order to break the circular waiting pattern.
Philosopher: acquire(leftFork) acquire(rightFork) eat() release(rightFork) release(leftFork)
Each philosopher competes for two neighboring forks. An unsafe acquisition order can create circular waiting.
first = smaller(leftFork, rightFork) second = larger(leftFork, rightFork) acquire(first) acquire(second) eat() release(second) release(first)
This ordering is one conceptual strategy for breaking the circular resource-order pattern. Real implementations can use different approaches depending on their fairness and performance requirements.
| Tool | Core Idea |
|---|---|
| Critical Section | The specific portion of code where shared data is accessed and must be protected |
| Mutex | A simple lock allowing only one process into a critical section at a time |
| Semaphore | An integer-based tool, controlled through wait() and signal(), for managing single or multiple shared resources |
| Monitor | A higher-level construct bundling shared data with the procedures that access it, enforcing exclusion automatically |
To make the theory useful beyond examination definitions, take a shared counter as a mini debugging exercise. First run two workers without protection and record the expected number of updates. Then protect the update with a mutex and compare the result. The point is not to depend on one specific programming language; the point is to recognize the same concurrency pattern across languages and systems.
Identify the variable, queue, buffer, file or other resource accessed by more than one execution unit.
Write the read, modify and write operations in separate steps. Look for an ordering that loses or corrupts an update.
Mark the critical section. Avoid placing unrelated work inside the protected region because unnecessary locking reduces concurrency.
Check mutual exclusion, progress, bounded waiting, deadlock and starvation instead of assuming that a lock automatically makes every design correct.
| Mistake | Correct Practice |
|---|---|
| Assuming a mutex and a binary semaphore are always exactly identical in every situation. | Remember that a mutex is typically released only by the process that locked it, while a semaphore can be signalled by a different process than the one that waited. |
| Believing a race condition only happens with obviously complex shared data structures. | Understand that even a single shared counter variable can produce a race condition if its increment isn't properly protected. |
| Treating the Dining Philosophers problem as unrelated to real synchronization scenarios. | Recognise that it models a very real and common situation: multiple processes each needing more than one shared resource at once. |
Process synchronization addresses what happens when multiple processes or threads need to safely share data without interfering with one another. We looked at how race conditions arise, what the critical section problem actually requires of a correct solution, and how tools like mutexes, semaphores, and monitors are used to enforce that safety in practice. We also walked through three classical synchronization problems — Producer-Consumer, Readers-Writers, and Dining Philosophers — each illustrating a different kind of coordination challenge that these tools are designed to solve.
With a solid understanding of how processes coordinate safe access to shared resources, you're now ready to move into Deadlock, which examines what happens when that coordination goes wrong, and processes end up waiting on each other indefinitely, along with the techniques used to prevent, avoid, and detect this situation.