DSA, Database System & Operating System β Operating System and Process Management, NEC licence examination syllabus (Nepal Engineering Council).
Critical Region, Race Condition, Mutual Exclusion
The section of code that must not be interleaved β and the four ways to protect it, measured.
π Where this lives: the fastest correct solution below is not a lock at all β it is not sharing. That principle drives real architecture: Nginx runs one worker per core with no shared state, Redis is single-threaded on purpose, and Go's slogan is "share memory by communicating" rather than the reverse. Locks are the tool of last resort, not the default. When you see a system that scales linearly with cores, it almost always got there by partitioning data so no lock is needed. Search "shared nothing architecture" and "false sharing cache line".
Definitions, precisely
CRITICAL SECTION (critical region)
a segment of code that accesses SHARED data and must not be
executed by more than one process at a time.
Note what it is NOT: it is not "code that uses a variable".
A local variable needs no protection. The definition requires
BOTH sharing AND at least one writer.
read-only sharing β no critical section needed
each process own copy β no critical section needed
shared + any writer β critical section
RACE CONDITION
a situation where the RESULT depends on the relative timing
or interleaving of operations. The output is not a function
of the input alone.
MUTUAL EXCLUSION (mutex)
the property that at most one process is inside its critical
section at any instant. It is the requirement; a lock is one
mechanism for achieving it.
THE FOUR-PART STRUCTURE every solution has:
do {
ENTRY SECTION request permission to enter
CRITICAL SECTION access the shared data
EXIT SECTION release permission
REMAINDER everything else
} while (true);
Bugs hide in the entry and exit sections, not the critical
section itself. Forgetting to release in the exit section is
how a deadlock is created; releasing too early is how mutual
exclusion is lost.
THE THREE REQUIREMENTS, restated (from the concurrency topic):
1. MUTUAL EXCLUSION one at a time
2. PROGRESS if nobody is inside, someone can enter
3. BOUNDED WAITING a waiting process eventually gets in
Four solutions, measured on the same problem
THE PROBLEM: 8 threads each increment a shared counter 200,000
times. Expected total 1,600,000. All figures MEASURED on this
machine (10 cores, Darwin arm64).
approach result correct? time
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
plain counter++ 1,000,000 NO 0.2 ms
atomic_fetch_add 1,600,000 yes 14.7 ms
pthread_mutex 1,600,000 yes 28.8 ms
per-thread + final sum 1,600,000 yes 0.1 ms
READ THAT TABLE CAREFULLY β it contains three lessons.
1. THE BROKEN VERSION IS THE FASTEST. 0.2 ms and wrong. Any
benchmark that does not check correctness will happily
recommend the racy code. Correctness first, then measure.
2. THE ATOMIC IS 2Γ FASTER THAN THE MUTEX (14.7 vs 28.8 ms).
atomic_fetch_add is ONE instruction with a lock prefix;
pthread_mutex_lock is a function call plus a CAS plus
potential blocking. For a single-word update, prefer the
atomic.
3. NOT SHARING BEATS EVERYTHING β 0.1 ms, faster than the
racy version, and CORRECT. Each thread increments its own
variable; the totals are summed once at the end. That is
288Γ faster than the mutex.
WHY: with a shared counter, the cache line holding it must
bounce between all 10 cores' caches on every increment β
hundreds of cycles each time. With per-thread counters, each
core writes only to its own cache line and never invalidates
another's. The cost was never the lock; it was the CACHE
COHERENCE traffic the sharing caused.
This is the single most useful performance insight in
concurrent programming: the question is not "which lock is
fastest" but "can I avoid sharing at all".
CAVEAT β FALSE SHARING: if the per-thread counters sit in the
SAME cache line (an array of longs is 8 bytes apart, a cache
line is 64β128 bytes), the coherence traffic returns even
though the variables are logically separate. The fix is to
pad each counter to its own cache line:
struct { long v; char pad[120]; } counters[THREADS];
That is why the measured 0.1 ms could be even better with
padding, and why "false sharing" is worth knowing by name.
Point 1 deserves emphasis because it is a trap people fall into with real profilers. The racy version is genuinely faster, and a benchmark that only measures time will rank it first. Any performance comparison of concurrent code must assert the result before reporting the timing β otherwise you are measuring how fast you can compute the wrong answer.
Software solutions to mutual exclusion
1. DISABLING INTERRUPTS
disable_interrupts();
critical section
enable_interrupts();
β trivially correct on a UNIPROCESSOR β no switch can occur
β USELESS on a multiprocessor: it stops interrupts on THIS
core only; another core continues freely
β dangerous β a bug in the critical section freezes the
machine
β cannot be given to user processes at all
Used only inside the kernel, for very short sections.
2. LOCK VARIABLE (naive)
while (lock == 1) ; /* wait */
lock = 1; /* acquire */
critical section
lock = 0;
β BROKEN. The test and the set are two operations, so both
processes can pass the while loop before either sets the
lock. This is the same load-modify-store problem as
counter++ β the lock itself has a race.
3. STRICT ALTERNATION (turn variable)
β violates PROGRESS: forces alternation even when the other
process does not want to enter.
4. PETERSON'S ALGORITHM
β satisfies all three requirements for two processes
β needs memory barriers on real hardware (measured in the
concurrency topic: ~50% of updates lost without them)
β awkward to extend beyond two processes (Bakery algorithm
generalises it)
5. HARDWARE ATOMIC INSTRUCTIONS β what is actually used
TEST-AND-SET, COMPARE-AND-SWAP, FETCH-AND-ADD
β correct, simple, works for n processes and n CPUs
β the foundation for every real mutex and semaphore
SPINLOCK built on test-and-set:
while (TestAndSet(&lock)) ; /* spin until acquired */
critical section
lock = false;
β no context switch β right when the section is very short
β BUSY WAITING wastes CPU; right only when the expected wait
is shorter than a context switch (~1β10 Β΅s)
β does NOT guarantee bounded waiting on its own β an
unlucky process can lose the race repeatedly
6. HIGHER-LEVEL PRIMITIVES (next topics)
semaphores, mutexes, monitors, condition variables
THE PRACTICAL RULE, in order of preference:
1. avoid sharing (per-thread data, immutability, message
passing) β measured 288Γ faster than a mutex
2. use an atomic if a single word is enough β measured 2Γ
faster than a mutex
3. use the library mutex β correct, portable, includes the
right barriers
4. hand-roll a lock only inside a kernel, and only if you
must
Priority inversion β when mutual exclusion goes wrong
Mutual exclusion introduces a new failure mode: a
high-priority process can be blocked, indirectly, by a
low-priority one.
LOW acquires the lock
HIGH preempts LOW, needs the lock β BLOCKS
MED preempts LOW (it does not need the lock)
β HIGH now waits for MED, which has LOWER priority than it
HIGH's wait is bounded only by how long MED runs, which is
unbounded. Hence UNBOUNDED PRIORITY INVERSION.
This is the Mars Pathfinder bug from the OS-types topic. The
fix is PRIORITY INHERITANCE: while LOW holds a lock that
HIGH wants, LOW temporarily runs at HIGH's priority, so MED
cannot preempt it. HIGH's wait then becomes bounded by LOW's
critical section length β which the programmer controls.
THE THREE HAZARDS OF MUTUAL EXCLUSION, together:
DEADLOCK two processes each hold what the other needs
STARVATION a process is perpetually denied the lock
PRIORITY a high-priority process blocked by a low one
INVERSION
All three are introduced BY the solution to the original
problem. That is the recurring theme of this section:
synchronisation does not remove difficulty, it relocates it β
which is the real reason "avoid sharing" is the first rule
rather than a micro-optimisation.
π Go further: the languages that took this seriously changed the type system rather than adding another lock. Rust makes data races impossible to compile: a value has either one mutable reference or many immutable ones, checked at compile time, so the counter++ race cannot be written. Go provides channels and a race detector (go test -race) that flags unsynchronised access on any run. Erlang gives processes no shared memory at all β only message passing β which is why it is used for systems that must not stop. Search "Rust fearless concurrency ownership" and "Go race detector".
π‘ Exam angle: define critical section precisely (shared data plus at least one writer) and give the four-part structure β entry, critical, exit, remainder. State the three requirements and which naive attempt violates each. Know why the naive lock variable is broken (test and set are two operations, so the lock itself has a race) and why disabling interrupts fails on a multiprocessor. Describe a spinlock built on test-and-set and when busy-waiting is the right choice. Priority inversion with priority inheritance as the fix is a strong closing point.
Syllabus points
Critical section problem
Race condition; mutual exclusion
Create a free account to tick topics off, take notes as you read, watch the video lessons and get a day-by-day study plan built around your exam date.
Related topics in Operating System and Process Management