When Processes Need to Cooperate
Processes don't always work in isolation. Sometimes they need to share data, coordinate actions, or wait for each other. For example, a web server might have one process handling incoming requests and another process writing to a database. They need to communicate without stepping on each other's toes.
This is where process synchronization comes in β the set of mechanisms the OS provides for processes to safely share resources and coordinate their activities.
The Race Condition
Without synchronization, disaster strikes. Imagine two processes trying to increment the same counter variable at the same time:
- Process A reads the counter value: 5
- Process B reads the counter value: 5
- Process A increments and writes: 6
- Process B increments and writes: 6
The counter should be 7 (both incremented it), but it's 6 because both processes read the same value before either wrote back. This is a race condition β the outcome depends on the unpredictable timing of process execution.
Race Condition (Counter Increment):
Time β Process A β Process B β Counter Value
ββββββββΌβββββββββββββββββββΌβββββββββββββββββββΌββββββββββββββ
t1 β Read counter (5) β β 5
t2 β β Read counter (5) β 5
t3 β Add 1 β 6 β β 5
t4 β Write 6 β β 6
t5 β β Add 1 β 6 β 6
t6 β β Write 6 β 6 β Should be 7!
ββββββββ΄βββββββββββββββββββ΄βββββββββββββββββββ΄ββββββββββββββ
Both processes read 5 before either writes. Result: lost update.
Race conditions are insidious because they're intermittent.
Race conditions are insidious because they're intermittent. The code might work 999 times and fail once β and that one failure could corrupt data, crash the system, or produce wrong results.
Mutex: The Bathroom Key
A mutex (mutual exclusion) is a synchronization primitive that ensures only one process can access a shared resource at a time. Think of it as a bathroom key β only one person can use the bathroom at a time. If someone is inside, the key is taken. Others must wait until it's available.
Before a process accesses a shared resource, it must "acquire" the mutex. If another process already holds the mutex, the requesting process waits. When the first process finishes, it "releases" the mutex, allowing the next process to proceed.
This eliminates race conditions by ensuring that only one process is in the critical section (the code that accesses shared resources) at a time.
Mutex in Action:
Time β Process A β Process B β Mutex
ββββββββΌβββββββββββββββββββββββΌβββββββββββββββββββββββΌβββββββ
t1 β acquire(mutex) β β β LOCKED
t2 β read counter (5) β acquire(mutex) β β LOCKED
t3 β counter++ β (waiting...) β LOCKED
t4 β write 6 β (waiting...) β LOCKED
t5 β release(mutex) β β FREE
t6 β β acquire(mutex) β β LOCKED
t7 β β read counter (6) β LOCKED
t8 β β counter++ β LOCKED
t9 β β write 7 β β LOCKED
t10 β β release(mutex) β FREE
Correct result: counter = 7
Semaphores: The Counting System
A semaphore is a more flexible synchronization tool. It's essentially a counter that controls access to a resource. There are two types:
- Binary Semaphore β Like a mutex, it has only two states: 0 (locked) and 1 (unlocked). Used for mutual exclusion.
- Counting Semaphore β Can have any non-negative value. It represents the number of available instances of a resource. For example, a counting semaphore initialized to 5 allows up to 5 processes to access the resource simultaneously.
Two operations are performed on semaphores: wait() (decrement the counter, block if zero) and signal() (increment the counter, wake a waiting process).
The Producer-Consumer Problem
A classic synchronization problem: one process (the producer) generates data and puts it into a buffer, while another process (the consumer) takes data from the buffer and processes it.
The challenges are:
- The producer must not add data when the buffer is full.
- The consumer must not try to take data when the buffer is empty.
- Both must not access the buffer simultaneously.
This is solved with two semaphores: one counting empty slots and one counting full slots. The producer waits on "empty" and signals "full." The consumer waits on "full" and signals "empty." This creates a perfectly coordinated dance where both processes work together without conflict.
Deadlock: When Everyone Waits
Deadlock occurs when two or more processes are each waiting for the other to release a resource, creating a circular wait that never ends. Process A holds Resource 1 and waits for Resource 2, while Process B holds Resource 2 and waits for Resource 1. Neither can proceed.
Deadlock Illustration:
ββββββββββββββββ ββββββββββββββββ
β Process A β β Process B β
ββββββββ¬ββββββββ βββββββββ¬βββββββ
β β
β Holds Holdsβ
β Resource 1 Resource 2
β β
β Wants Wantsβ
βΌ βΌ
ββββββββββββββββ ββββββββββββββββ
β Resource 2 ββββββββββββββββ Resource 1 β
ββββββββββββββββ ββββββββββββββββ
Circular Wait: A β Resource 2 β B β Resource 1 β A β ...
Neither process can proceed. Deadlock!
Deadlock requires four conditions to occur simultaneously:
Deadlock requires four conditions to occur simultaneously: mutual exclusion, hold and wait, no preemption, and circular wait. The OS prevents deadlock by breaking one of these conditions β for example, requiring processes to request all resources at once, or forcibly reclaiming resources from waiting processes.