Hardware Synchronization, Mutex Locks, and Semaphores
Memory Barriers and Hardware Synchronization Foundations
Memory barriers (also known as memory fences) provide a basic architectural instruction that guarantees all preceding instructions are completely executed before any subsequent instructions are permitted to execute.
Hardware synchronization support is an absolute prerequisite for implementing higher-level software synchronization tools such as mutex locks and semaphores; without underlying processor support, synchronization cannot be guaranteed.
The core requirement of synchronization hardware is providing atomic instructions that guarantee that when a shared status or lock value is modified, the operation executes without interruption and completely blocks competing updates from other processors or cores.
In modern multi-core and multiprocessor architectures, multiple threads or processes can be scheduled simultaneously across distinct execution cores; multiple instructions can be initiated at the exact same clock cycle.
The Central Processing Unit (CPU) must enforce at the hardware level that if multiple processors attempt to modify a synchronization variable concurrently, exactly one process succeeds in making progress, while all competing processes are suspended or prevented from altering the state.
Critical-Section Problem and Architectural Solutions
The critical-section problem requires coordinating processes that access shared variables or data structures such as a critical section.
The complete structural layout of a cooperating process consists of four sequential sections:
Entry Section: The code preceding the critical section where a process requests permission to access the critical resource.
Critical Section: The block of code where shared resources, variables, or files are accessed and modified.
Exit Section: The code immediately following the critical section where the lock or resource status is released.
Remainder Section: The remaining operational code executed by the process after leaving the exit section.
Any valid solution to the critical-section problem must satisfy three fundamental requirements:
Mutual Exclusion: If a process is executing in its critical section, no other processes are allowed to execute in their critical sections simultaneously.
Progress: If no process is executing in its critical section and there are processes wishing to enter, only those processes not executing in their remainder sections can participate in deciding which process enters next, and this selection cannot be postponed indefinitely.
Bounded Waiting: A bound or limit must exist on the number of times other processes are allowed to enter their critical sections after a process has made an entry request and before that request is granted, preventing process starvation.
Locking mechanism abstraction:
Entry section acquires the lock by verifying whether it is free; if free, it takes ownership of the lock.
Exit section releases the lock, restoring its status so other waiting processes can acquire it.
Lock status is represented by a shared variable (such as a boolean or an integer).
In pass-by-reference parameter passing (signified in C-style syntax by the address-of operator
&, such as&lock), the memory address of the variable is passed to the function, enabling direct in-place modification of the memory cell.Pass-by-value merely provides a local copy to the receiving function, leaving the original memory address unaltered.
Hardware Instruction: Test and Set
test_and_setis a low-level atomic hardware instruction provided by CPU instruction sets under various architecture-specific instruction names.Internal behavioral definition of
test_and_set:Accepts a pointer to a target boolean variable:
boolean test_and_set(boolean *target)Copies the original value stored at the target address into a local return variable:
boolean rv = *target;Overwrites the memory cell of target with
true:*target = true;Returns the original value stored in
rv:return rv;Entire sequence executes atomically; the CPU guarantees it cannot be preempted or interrupted mid-execution.
Implementation of mutual exclusion using
test_and_set:A shared boolean variable
lockis initialized tofalse(wherefalseindicates the critical section is unlocked and open).Process entry section executes a busy-waiting spin lock:
while (test_and_set(&lock));Process exit section executes:
lock = false;
Execution trace for a single process entering the critical section:
Initial state:
lock = false.Process invokes
test_and_set(&lock).Inside
test_and_set,rvreceivesfalse,lockis updated totrue, and the instruction returnsfalse.The
whileloop condition receivesfalse, terminating the loop immediately.The process enters and executes its critical section.
Execution trace for competing processes attempting entry while
lock = true:Competing processes invoke
test_and_set(&lock).Because
lockis alreadytrue,rvreceivestrue,lockremains written astrue, andtest_and_setreturnstrue.The
whileloop condition evaluates totrue, causing competing processes to repeatedly spin in thewhileloop (busy waiting / spin lock).When the owning process executes the exit section (
lock = false;), the lock returns tofalse.At that point, the next execution of
test_and_set(&lock)by a waiting process will returnfalse, allowing that process to exit the loop and enter the critical section.
Hardware arbitration in multi-core systems:
If two or more cores initiate
test_and_seton the identical memory address at the exact same clock cycle, hardware bus arbitration or memory locking guarantees that exactly one core executes the instruction first and receivesfalse.The remaining cores execute sequentially after the lock value has transitioned to
true, receivingtrueand remaining trapped in their spin locks.
Hardware Instruction: Compare and Swap
compare_and_swapis an alternative hardware instruction operating on integer variables that performs atomic comparison and conditional assignment.Internal behavioral definition of
compare_and_swap:Accepts three parameters: a pointer to an integer variable, an expected value, and a new target value:
int compare_and_swap(int *value, int expected, int new_value)Preserves the current value in a temporary local variable:
int temp = *value;Conditionally evaluates whether the current value matches
expected:if (*value == expected) *value = new_value;Returns the original value:
return temp;Executed as an uninterruptible, atomic hardware instruction.
Implementation of mutual exclusion using
compare_and_swap:A shared integer variable
lockis initialized to .Process entry section executes:
while (compare_and_swap(&lock, 0, 1) != 0);Process exit section executes:
lock = 0;
Execution trace of
compare_and_swap:When
lockis , a process callscompare_and_swap(&lock, 0, 1).tempis assigned .The condition
*value == expectedevaluates to0 == 0(true);lockis updated to .The function returns .
The loop condition
0 != 0evaluates to false, terminating the spin lock and admitting the process to the critical section.If subsequent processes execute
compare_and_swap(&lock, 0, 1)whilelockis ,tempreceives , the condition1 == 0evaluates to false (leavinglockunchanged at ), and the function returns .The loop condition
1 != 0evaluates to true, holding competing processes in a busy wait until the lock is reset to in the exit section.
Bounded-Waiting Mutual Exclusion Algorithm Using Test and Set
While simple
test_and_setandcompare_and_swaploops guarantee mutual exclusion and progress, they do not guarantee bounded waiting because process selection among spinning threads is arbitrary.An enhanced algorithm utilizing
test_and_setsatisfies all three requirements (Mutual Exclusion, Progress, and Bounded Waiting) for processes.Shared data structures and initializations:
boolean waiting[n];where every element is initialized tofalse.boolean lock;initialized tofalse.Each process maintains a local boolean variable
key.
Algorithmic structure for process :
Entry Section:
waiting[i] = true;key = true;while (waiting[i] && key)key = test_and_set(&lock);waiting[i] = false;Critical Section:
Code accessing shared resources.
Exit Section:
j = (i + 1) % n;while ((j != i) && !waiting[j])j = (j + 1) % n;if (j == i)lock = false;elsewaiting[j] = false;Remainder Section:
Non-critical process operations.
Detailed trace of the entry section:
Process announces its intent to enter by asserting
waiting[i] = trueand initialkey = true.It enters the
whileloop, executingkey = test_and_set(&lock).If
lockwasfalse,test_and_setreturnsfalse, causingkeyto becomefalse.The condition
waiting[i] && keyevaluates totrue && false, which is false, exiting the loop.Process sets
waiting[i] = falseand enters the critical section.If other processes attempt to enter while is in the critical section,
lockistrue, sotest_and_set(&lock)consistently returnstrue.Because
waiting[j]remainstrueandkeyremainstrue,waiting[j] && keyevaluates totrue && true, confining all competing processes to their spin locks.
Detailed trace of the exit section and handover:
Upon completing its critical section, process scans the
waitingarray in circular order starting at index .The loop
while ((j != i) && !waiting[j])advances cyclically as long as process is not waiting (!waiting[j]is true).If no processes are waiting, the cycle completes such that . Process sets
lock = false, leaving the lock available for any future process.If a waiting process is discovered (), the loop terminates with .
Process executes
waiting[j] = false;without resettinglocktofalse.
Verification of the three requirements:
Mutual Exclusion: A process can enter the critical section only if
waiting[i] == falseorkey == false. When a process enters,lockistrue. Even if another process resets its ownkey,lockremainstrue. Only the designated process chosen by the exit section has itswaitingflag set tofalse, guaranteeing only one process enters at any time.Progress: If a process leaves the critical section and other processes are waiting, the exit section designates a specific waiting process or sets
lock = false, ensuring an unblocked process immediately enters without hindrance.Bounded Waiting: The cyclic scan () ensures that any waiting process will be serviced within at most turns, eliminating indefinite postponement or starvation.
Mutex Locks
Mutex (mutual exclusion) locks represent a software-level synchronization tool built upon hardware atomic primitives to protect critical sections.
A mutex lock utilizes a boolean variable
availableindicating whether the lock is obtainable.Two fundamental operations manage the mutex lock:
acquire(): Invoked in the entry section to obtain the lock.release(): Invoked in the exit section to relinquish the lock.
Structural implementation of
acquire():acquire() {while (!available); // busy waitavailable = false;}The testing of
availableand its transition tofalsemust be executed atomically via underlying instructions liketest_and_setto prevent multiple threads from acquiring the lock concurrently.
Structural implementation of
release():release() {available = true;}
A mutex lock implemented with busy waiting is classified as a spinlock; processes repeatedly execute a loop checking the availability condition, consuming CPU cycles while waiting.
Semaphores: Definition, Operations, and Types
A semaphore is a synchronization tool represented by an integer variable .
Classical synchronization defines two standard atomic operations on a semaphore:
wait()andsignal()(historically designated as for proberen, to test, and for verhogen, to increment).Definition of
wait(S)(utilized in the entry section):wait(S) {while (S <= 0); // busy waitS--;}If , the executing process is blocked in a busy wait; once , the process decrements and proceeds.
Definition of
signal(S)(utilized in the exit section):signal(S) {S++;}Increments the value of semaphore atomically.
All modifications to the integer value of in
wait()andsignal()must execute atomically and indivisibly without preemption.Classification of semaphores based on integer range:
Binary Semaphore: The value of is restricted to and . A binary semaphore functions identically to a mutex lock, where denotes available and denotes locked.
Counting Semaphore: The value of can range over an unrestricted domain (), used to control access to a finite set of identical resources.
Controlling Execution Path Using Semaphores
Semaphores can enforce deterministic execution orderings across concurrent processes, guaranteeing that specific statements execute in a defined sequence regardless of CPU scheduling order.
Scenario: Enforce that statement in process executes strictly before statement in process .
Mechanism:
Define a shared synchronization semaphore:
semaphore synch;initialized to .Process code structure:
S1;signal(synch);Process code structure:
wait(synch);S2;
Execution trace:
If is scheduled first, it executes
wait(synch). Becausesynchis , the conditionsynch <= 0holds, forcing to busy-wait.When executes, it completes statement and executes
signal(synch), which incrementssynchfrom to .detects
synch == 1, exits itswaitloop, decrementssynchback to , and executes statement .If is scheduled first, it completes , signals
synch(setting it to ), and when arrives atwait(synch), it proceeds immediately without waiting.
Questions & Discussion: In-Class Exercise 7
Problem Formulation:
Design a synchronization scheme using semaphores for four processes (, , , and ) submitted to the ready queue in arbitrary order (e.g., , then , then , then ).
Execution Constraint: Process must always execute first. Once completes, processes and can execute concurrently. Process can start execution only after both and have fully completed their work.
Implementation Steps Proposed:
To block and until finishes, declare two separate semaphores,
aandb, both initialized to :semaphore a = 0;semaphore b = 0;Process executes
wait(a)at its entry.Process executes
wait(b)at its entry.
Dialogue on Coordinating Completion:
Prompt: Which process should call
signal(b)andsignal(c)?Response: Process must call
signal(a)andsignal(b)upon completing its execution.Prompt: How should and signal the completion of their tasks to enable ?
Response: Processes and could both invoke
signal(c)on a shared semaphorec(initialized to ), while precedes its execution withwait(c).
Inherent Race Condition Flaw Identified:
In a proposed scheme where calls
signal(c), callssignal(c), and executes a singlewait(c):If process finishes its job while process is still executing, invokes
signal(c), incrementingcfrom to .Process , blocked at
wait(c), seesc == 1, decrementscto , and proceeds to execute its task.Consequently, process and process run concurrently, directly violating the prerequisite condition that must wait until both and have finished.
Resolving this race condition requires an additional synchronization mechanism (such as multiple semaphores, barrier synchronization, or a counter) to ensure is unblocked only when both predecessors have finished.