Process Synchronization and Critical Section Problem
This chapter explores how modern operating systems manage concurrent execution among multiple processes or threads. We examine the fundamental challenges of shared data access, race conditions, classical requirements for synchronization, and practical solutions including Peterson's algorithm, mutex locks, and semaphores.
1. Why Do We Need Process Synchronization?
In a multi-programming operating system, multiple processes or threads execute concurrently to maximize CPU utilization and throughput. However, when these concurrent processes attempt to access and modify shared resources (such as global variables, shared memory regions, or files) without coordination, unpredictable behavior and data corruption occur.
Real-Life Analogy: The Shared Bank Account
Imagine two people (Process A and Process B) sharing a single bank account balance of $1,000.
- Person A wants to deposit
$500. - Person B wants to withdraw
$200at the exact same time.
If both people read the initial balance ($1,000) simultaneously:
- Person A calculates: 500 = 1,500` to the account.
- Person B calculates: 200 = 800` to the account.
Depending on which write operation finishes last, the final balance will be either $1,500 or $800. The deposit made by Person A is completely lost in the second scenario. This timing-dependent anomaly is what operating systems call a Race Condition.
2. The Critical Section Problem
To prevent race conditions, we must isolate the portion of code that accesses shared resources.
- Critical Section: The code segment within a process that accesses shared resources (variables, files, databases).
- Race Condition: A situation where multiple processes access and manipulate shared data concurrently, and the final outcome depends on the particular order of execution.
General Structure of a Typical Process
do {
// Entry Section (Request permission to enter critical section)
// Critical Section (Access shared data)
// Exit Section (Release permission, notify others)
// Remainder Section (Remaining non-critical code)
} while (true);
3. Requirements for a Valid Critical Section Solution
Any valid solution to the critical section problem must satisfy three strict requirements:
4. Peterson's Solution
Peterson's Solution is a classic software-based solution to the critical section problem for two processes ( and ). It relies on two shared variables:
boolean flag[2]; // Indicates if a process is ready to enter the critical section
int turn; // Whose turn is it to enter the critical section
Algorithm Structure for Process (where )
do {
flag[i] = true;
turn = j;
// Busy waiting: wait while the other process wants to enter AND it is their turn
while (flag[j] && turn == j);
// --- CRITICAL SECTION ---
flag[i] = false; // Exit section
// --- REMAINDER SECTION ---
} while (true);
Note: Peterson's solution successfully satisfies all three conditions (Mutual Exclusion, Progress, and Bounded Waiting) for two processes. However, modern computer architectures (with out-of-order execution and aggressive CPU caching) can break software-based solutions like Peterson's unless memory barriers are explicitly enforced.
5. Locks and Hardware Synchronization Primitives
Because software solutions can be complex and inefficient on modern multi-core processors, operating systems and hardware architectures provide low-level synchronization primitives.
Mutex (Mutual Exclusion Lock)
A Mutex is a synchronization lock used to protect shared resources.
- It acts as a binary flag with two states:
0(Unlocked) and1(Locked). - Ownership Rule: Only the specific thread that locked the mutex can unlock it.
- Busy Waiting (Spinlock): When a thread attempts to acquire an already locked mutex, it may continuously loop (
spin) checking if the lock has been released, consuming CPU cycles until the lock becomes available.
6. Semaphores
A Semaphore is a more powerful synchronization tool than a simple mutex. It is an integer variable accessed exclusively through two atomic, uninterrupted operations: wait() and signal() (historically known as P() and V()).
Core Semaphore Operations
wait(S):
S.value--;
if (S.value < 0) {
// Add this process to S.queue();
// block();
}
signal(S):
S.value++;
if (S.value <= 0) {
// Remove a process P from S.queue();
// wakeup(P);
}
- If
S.valuedrops below0, the calling process is blocked and placed into a waiting queue (avoiding CPU waste from busy waiting). - When
signal()incrementsS.valueback to , a waiting process is awakened.
Types of Semaphores
- Binary Semaphore: Can only take values
0or1. Functions similarly to a Mutex, though mutexes carry ownership semantics (only the owner can unlock), whereas semaphores can be signaled by any thread. - Counting Semaphore: Can range over an unrestricted integer domain. Used to control access to a finite pool of identical resources (e.g., managing connection pools or database handles with a capacity of ).
7. Summary and Key Takeaways
- Race Conditions: Occur when multiple concurrent processes or threads access shared data simultaneously, leading to unpredictable and corrupted outcomes.
- Critical Section: The specific segment of code that accesses shared mutable resources and requires exclusive execution.
- Three Essential Requirements: Any valid solution must guarantee Mutual Exclusion, Progress, and Bounded Waiting.
- Peterson's Solution: A classic software-based synchronization algorithm for two processes utilizing
flagandturnvariables. - Mutex Locks: Binary synchronization primitives used to lock shared resources, where only the locking thread can release the lock.
- Spinlocks: Locks that use busy waiting, continuously polling the lock state rather than putting the thread to sleep.
- Semaphores: Integer-based synchronization primitives managed via atomic
wait()andsignal()operations that can handle multi-resource allocation without busy waiting. - Binary vs. Counting Semaphores: Binary semaphores restrict values to
0and1, while counting semaphores support arbitrary integer ranges to limit concurrency across resource pools.