Process Synchronization
Race conditions, the critical-section problem, Peterson's solution, hardware locks and semaphores.
When processes or threads share data, the result can depend on the exact order in which their instructions interleave. That is a race condition, and it causes some of the hardest bugs to find.
A race condition, step by step
count++ looks like one step but compiles to three machine instructions: load count into a register, add 1, store it back. If two threads interleave those steps, one update can be lost. The simulation below replays one such interleaving deterministically.
count = 5
reg_a = reg_b = None
schedule = [
("A", "load"), ("B", "load"), # both read 5
("A", "add"), ("B", "add"), # both compute 6
("A", "store"), ("B", "store"), # both write 6
]
for thread, step in schedule:
if step == "load":
if thread == "A": reg_a = count
else: reg_b = count
elif step == "add":
if thread == "A": reg_a += 1
else: reg_b += 1
else:
count = reg_a if thread == "A" else reg_b
print(f"{thread} {step:5} -> count = {count}")
print("Two increments, but count went from 5 to", count)
A load -> count = 5 B load -> count = 5 A add -> count = 5 B add -> count = 5 A store -> count = 6 B store -> count = 6 Two increments, but count went from 5 to 6
The critical-section problem
The part of the code that touches shared data is the critical section. A correct solution must guarantee three properties:
- Mutual exclusion: at most one process is in its critical section at a time.
- Progress: if nobody is in the critical section, the choice of who enters next cannot be postponed forever, and only processes that want to enter take part in the choice.
- Bounded waiting: after a process asks to enter, there is a limit on how many times others can enter before it.
Peterson's solution (two processes)
// shared: bool flag[2] = {false, false}; int turn;
// code for process i (the other process is j = 1 - i)
flag[i] = true; // I want to enter
turn = j; // but you go first if you also want to
while (flag[j] && turn == j)
; // busy wait
// ---- critical section ----
flag[i] = false; // I am done
Peterson's algorithm satisfies all three properties for two processes. It uses busy waiting (spinning in a loop), and on modern CPUs it needs memory barriers because the hardware may reorder the loads and stores.
Hardware support: test-and-set
A test-and-set instruction atomically reads a memory word and sets it to true, returning the old value. A spin lock built on it:
// shared: bool lock = false;
while (test_and_set(&lock))
; // spin until the old value was false
// ---- critical section ----
lock = false;
This gives mutual exclusion but not bounded waiting: an unlucky process can keep losing the race.
Semaphores
A semaphore S is an integer accessed only through two atomic operations:
- wait(S) (also called P or down): decrement S; if the result is negative, the caller blocks.
- signal(S) (also called V or up): increment S; if a process is blocked on S, wake one up.
A binary semaphore (values 0 and 1, also called a mutex) protects a critical section. A counting semaphore initialised to n allows up to n processes in at once, which is useful for pools of identical resources.
// mutual exclusion with a semaphore
semaphore mutex = 1;
wait(mutex);
// ---- critical section ----
signal(mutex);
Semaphores can also enforce ordering: to make statement S2 in process B run after S1 in process A, use semaphore done = 0; A runs S1; signal(done); and B runs wait(done); S2;.
# Tracking the value of a counting semaphore through a sequence of operations
S = 7
ops = ["P"] * 20 + ["V"] * 15
for op in ops:
S += -1 if op == "P" else 1
print("final value of S:", S)
final value of S: 2
When a semaphore's value is negative, its magnitude is the number of processes blocked on it.
GATE-style practice questions
A counting semaphore S is initialised to 10. Then 12 wait (P) operations and 7 signal (V) operations are performed in some order. What is the final value of S?
Show answer and explanation
Answer: 5Each P subtracts 1 and each V adds 1, whatever the order, so the final value is 10 − 12 + 7 = 5.
Which properties does Peterson's algorithm guarantee for two processes? (Select all that apply.)
Show answer and explanation
Answer: A, B, CPeterson's solution satisfies all three critical-section requirements. It does busy-wait: a process loops on while (flag[j] && turn == j) until it may enter.
Two threads each execute x = x + 1 once on a shared variable x that starts at 0, without any synchronization. Which final values of x are possible? (Select all that apply.)
Show answer and explanation
Answer: A, BIf one thread finishes its load-add-store before the other starts, x becomes 2. If both load 0 before either stores, both store 1 and one update is lost. x cannot stay 0 (someone stores a value of at least 1) and cannot exceed 2.
A lock is built with the test-and-set instruction as while (test_and_set(&lock)); /* CS */ lock = false;. Which requirement can it violate?
Show answer and explanation
Answer: CTest-and-set is atomic, so two processes cannot both see false: mutual exclusion holds. But nothing forces fairness; a process can keep losing the race to others, so bounded waiting is not guaranteed.