Process Synchronization and Critical Section
AI-Generated Content
Process Synchronization and Critical Section
When multiple threads or processes in a system need to access shared resources like variables, files, or memory, uncontrolled access leads to unpredictable and incorrect outcomes. Understanding how to coordinate this access—a discipline known as process synchronization—is fundamental to building reliable operating systems, databases, and concurrent applications.
The Race Condition Problem
A race condition occurs when the final outcome of a computation depends on the unpredictable, interleaved order in which multiple processes execute their instructions while accessing shared data. Imagine two processes, P1 and P2, both trying to increment a shared variable counter (initial value 0). The increment operation in a high-level language like counter++ typically compiles to three low-level machine instructions: LOAD the value from memory into a register, INCREMENT the register, and STORE the result back to memory.
If the operating system scheduler interleaves these instructions, a problematic sequence can occur:
- P1: LOAD counter (gets 0)
- P1: INCREMENT register (register now holds 1)
- P2: LOAD counter (still 0)
- P2: INCREMENT register (register now holds 1)
- P1: STORE register (counter = 1)
- P2: STORE register (counter = 1)
Despite two increment operations, the final value of counter is 1, not 2. This inconsistency is the direct result of a race condition. The root cause is that the increment operation is not atomic—it can be interrupted partway through. The section of code where shared data is accessed is called the critical section. The core challenge of synchronization is to ensure that when one process is executing in its critical section, no other process is allowed to execute in its own critical section for the same shared resource.
Requirements for a Correct Solution
A valid solution to the critical section problem must satisfy three fundamental conditions:
- Mutual Exclusion: If a process is executing in its critical section, then no other processes can be executing in their critical sections for the same resource. This is the primary goal.
- Progress: If no process is in its critical section and some processes wish to enter theirs, the selection of which process enters next cannot be postponed indefinitely. The system must keep moving forward.
- Bounded Waiting: There exists a bound on the number of times other processes are allowed to enter their critical sections after a process has made a request to enter and before that request is granted. This prevents starvation, where a process is perpetually denied access.
These conditions apply to the entry section (code that requests permission to enter the critical section) and exit section (code that signals departure) that frame the critical section itself. The remainder of a process's code is the remainder section.
Software Solutions: Peterson's Algorithm
Before relying on hardware support, classic software algorithms were developed to solve the critical section problem for two processes. Peterson's solution is an elegant and correct algorithm that uses shared variables to achieve mutual exclusion without special hardware instructions.
It uses two shared variables: int turn and boolean flag[2] (initialized to false). The flag[i] variable indicates that process i is interested in entering its critical section. The turn variable indicates whose turn it is to proceed. The algorithm for process i (with the other process being j) is:
// Entry Section for Process i
flag[i] = TRUE; // I'm interested
turn = j; // Be polite, give turn to the other
while (flag[j] == TRUE && turn == j) {
// Busy wait
}
// CRITICAL SECTION HERE
// Exit Section for Process i
flag[i] = FALSE; // I'm no longer interested
// REMAINDER SECTIONThis works because a process only gets stuck in the while loop if the other process is both interested (flag[j] == TRUE) and it is the other process's turn (turn == j). By setting turn = j after declaring interest, process i defers to the other. If both processes try to enter simultaneously, the last write to turn wins, allowing only one to proceed. This solution satisfies mutual exclusion, progress, and bounded waiting for two processes. However, it involves busy waiting (the process spins in a loop consuming CPU time), and it does not easily generalize to more than two processes.
Hardware-Based Synchronization
Modern systems provide special atomic hardware instructions that simplify building synchronization primitives. An atomic operation completes in a single, uninterruptible step relative to other processors.
One common instruction is Test-and-Set. It atomically reads a memory location and writes a new value to it, returning the old value. A simple lock (or mutex) can be implemented using a boolean variable lock initialized to false:
boolean TestAndSet(boolean *target) {
boolean old = *target;
*target = TRUE;
return old;
}
// Entry Section using Test-and-Set
while (TestAndSet(&lock) == TRUE) {
// Busy wait (acquiring the lock)
}
// Critical Section
lock = FALSE; // Exit Section: release lockIf the lock was false (free), TestAndSet sets it to true and returns false, allowing the process to exit the loop and enter its critical section. If the lock was true (held), it returns true, and the process continues to loop. This ensures mutual exclusion. Another common instruction is Compare-and-Swap (CAS). While hardware solutions eliminate the complexity of software algorithms, they still often result in busy waiting, wasting CPU cycles.
Moving Beyond Busy Waiting: Semaphores and Mutexes
To avoid the CPU waste of busy-wait locks (or spinlocks), higher-level synchronization constructs are built using hardware support and operating system functionality. The most fundamental of these is the semaphore, an integer variable S that can only be accessed via two atomic operations: wait() (historically P()) and signal() (historically V()).
-
wait(S)decrementsS. IfSbecomes negative, the process executing thewait()is blocked and placed in a waiting queue associated with the semaphore. -
signal(S)incrementsS. IfSis still zero or negative, it wakes up one process from the waiting queue.
A binary semaphore (value 0 or 1) can be used directly as a mutex lock. A counting semaphore can control access to a resource with multiple identical instances. Crucially, when a process blocks on a semaphore, the OS scheduler puts it to sleep and runs another ready process, eliminating busy waiting. A mutex (short for mutual exclusion) is a simpler, specialized locking construct often implemented with a binary semaphore, providing operations acquire() (to enter the critical section) and release() (to exit).
Common Pitfalls
- Deadlock: This occurs when two or more processes are permanently blocked, each waiting for a resource held by the other. A classic example is when Process 1 holds Lock A and requests Lock B, while Process 2 holds Lock B and requests Lock A. Neither can proceed. Prevention requires strategies like enforcing a strict global order for acquiring multiple locks.
- Priority Inversion: In a priority-based scheduling system, a high-priority process may be forced to wait for a low-priority process that holds a needed lock. The situation worsens if a medium-priority process preempts the low-priority one, indirectly blocking the high-priority process indefinitely. Solutions include priority inheritance protocols, where the low-priority process temporarily assumes the high priority of any process waiting for its lock.
- Incorrect Synchronization Scope: Failing to protect all accesses to shared data or protecting non-shared data unnecessarily. The former leads to subtle race conditions; the latter degrades performance due to excessive serialization. Carefully identify the minimal critical section needed for correctness.
- Reliance on Software Alone for Complex Systems: While algorithms like Peterson's are excellent for understanding the problem, real-world multi-core and distributed systems require hardware-supported atomic operations and OS-managed primitives (like semaphores and mutexes) for efficiency and correctness.
Summary
- Race conditions arise from uncontrolled concurrent access to shared data, leading to non-deterministic and incorrect results. The code segment accessing shared data is the critical section.
- Any solution must guarantee mutual exclusion (only one process in the critical section), progress (a process will eventually enter), and bounded waiting (no process starves).
- Peterson's algorithm is a classic two-process software solution that uses shared variables to coordinate entry without hardware support, though it involves busy waiting.
- Hardware provides atomic instructions like Test-and-Set and Compare-and-Swap to build simple locks, but these can still lead to busy-waiting.
- High-level constructs like semaphores and mutexes use hardware support and OS scheduling to block waiting processes, eliminating CPU waste and forming the practical basis for synchronization in modern systems.