Chapter 6: Synchronization Tools - Comprehensive Notes
Chapter 6: Synchronization Tools
This chapter discusses synchronization tools, including background concepts, the critical-section problem, Peterson’s solution, hardware support, mutex locks, semaphores, monitors, liveness, and evaluation of these tools.
Objectives
Describe the critical-section problem and illustrate a race condition.
Illustrate hardware solutions using memory barriers, compare-and-swap operations, and atomic variables.
Demonstrate how mutex locks, semaphores, monitors, and condition variables solve the critical section problem.
Evaluate tools solving the critical-section problem in different contention scenarios.
Background
Processes can execute concurrently and may be interrupted at any time.
Concurrent access to shared data may lead to data inconsistency.
Mechanisms are required to ensure orderly execution of cooperating processes to maintain data consistency.
Chapter 4 illustrated the Bounded Buffer problem, which leads to a race condition when a counter is updated concurrently by a producer and consumer.
Race Condition
Processes P0 and P1 create child processes using the
fork()system call.A race condition occurs on the kernel variable
next_available_pid, which represents the next available process identifier.Without a mechanism to prevent simultaneous access, P0 and P1 might assign the same PID to two different processes.
Critical Section Problem
Consider a system of n processes {p0, p1, …, pn-1}.
Each process has a critical section segment of code where it may be changing common variables, updating a table, writing a file, etc.
When one process is in its critical section, no other process may be in its critical section.
The critical section problem involves designing a protocol to manage this.
A process must request permission to enter its critical section in the entry section, followed by the critical section, an exit section, and a remainder section, as illustrated in the provided diagram.
Critical Section Structure
The general structure of a process involves an entry section, critical section, exit section, and remainder section.
Requirements for Critical-Section Problem Solutions
Mutual Exclusion: If process is executing in its critical section, then no other processes can be executing in their critical sections.
Progress: If no process is executing in its critical section and some processes wish to enter, the selection of the process that will enter the critical section next cannot be postponed indefinitely.
Bounded Waiting: A limit must exist on the number of times other processes can enter their critical sections after a process has requested to enter its critical section and before that request is granted.
Assume each process executes at a non-zero speed.
No assumptions are made about the relative speeds of the n processes.
Interrupt-based Solution
Entry section: disable interrupts.
Exit section: enable interrupts.
Issues:
If the critical section's code runs for an extended period, it can be problematic.
Processes might starve, never entering their critical section.
This approach is unsuitable for systems with multiple CPUs.
Software Solution 1
Two-process solution.
Assumes load and store machine-language instructions are atomic.
Shared variable:
int turn;
The variable
turnindicates whose turn it is to enter the critical section. Initialized to .
Algorithm for Process Pi
The algorithm for process :
while (true) {
while (turn == j);
/* critical section */
turn = j;
/* remainder section */
}
Correctness of the Software Solution
Mutual exclusion is preserved:
enters the critical section only if
turn = i.turncannot be both 0 and 1 simultaneously.
Progress requirement: Not guaranteed.
Bounded-waiting requirement: Not guaranteed.
Peterson’s Solution
Two-process solution.
Assumes load and store machine-language instructions are atomic.
Shared variables:
int turn;(indicates whose turn it is to enter the critical section)boolean flag[2];(indicates if a process is ready to enter the critical section)flag[i] = trueimplies that process is ready.
Algorithm for Process Pi
while (true) {
flag[i] = true;
turn = j;
while (flag[j] && turn == j);
/* critical section */
flag[i] = false;
/* remainder section */
}
Correctness of Peterson’s Solution
The three critical section requirements are met:
Mutual exclusion: enters CS only if either
flag[j] = falseorturn = i.Progress: Guaranteed.
Bounded-waiting: Guaranteed.
Peterson’s Solution and Modern Architecture
Peterson’s Solution is not guaranteed to work on modern architectures due to potential reordering of operations by processors and compilers to improve performance.
This reordering can lead to race conditions.
Reordering is acceptable for single-threaded processes but can produce inconsistent or unexpected results in multithreaded scenarios.
Modern Architecture Example
Two threads share data:
boolean flag = false;int x = 0;
Thread 1:
while (!flag); print x;Thread 2:
x = 100; flag = true;Expected output:
100
Modern Architecture Example (Cont.)
Since
flagandxare independent, the instructions in Thread 2 (flag = true;andx = 100;) may be reordered.If reordered, the output may be
0.
Peterson’s Solution Revisited
Instruction reordering can cause Peterson’s Solution to fail, allowing both processes into their critical sections simultaneously.
To ensure correct operation on modern architectures, Memory Barriers must be used.
Memory Barrier
Memory model: memory guarantees a computer architecture makes to application programs.
Types:
Strongly ordered: Memory modification of one processor is immediately visible to all others.
Weakly ordered: Memory modification of one processor may not be immediately visible to all others.
A memory barrier is an instruction that forces any change in memory to be propagated (made visible) to all other processors.
Memory Barrier Instructions
When a memory barrier instruction is performed, the system ensures all loads and stores are completed before any subsequent load or store operations are performed.
Even if instructions were reordered, the memory barrier ensures that store operations are completed in memory and visible to other processors before future load or store operations are performed.
Memory Barrier Example
Using the previous example:
Thread 1:
while (!flag) memory_barrier(); print x;Thread 2:
x = 100; memory_barrier(); flag = true;For Thread 1, the value of
flagis loaded before the value ofx.For Thread 2, the assignment to
xoccurs before the assignment toflag.
Synchronization Hardware
Many systems provide hardware support for implementing critical section code.
Uniprocessors:
Could disable interrupts.
Currently running code would execute without preemption.
Generally too inefficient on multiprocessor systems and not broadly scalable.
Three forms of hardware support:
Hardware instructions
Atomic variables
Hardware Instructions
Special hardware instructions that allow atomic (uninterrupted) testing and modification of a word's content or swapping the contents of two words.
Test-and-SetinstructionCompare-and-Swapinstruction
The testandset Instruction
Definition:
boolean test_and_set (boolean *target) { boolean rv = *target; *target = true; return rv; }Properties:
Executed atomically.
Returns the original value of the passed parameter.
Sets the new value of the passed parameter to
true.
Solution Using testandset()
Shared boolean variable
lock, initialized tofalse.Solution:
do { while (test_and_set(&lock)) ; /* critical section */ lock = false; /* remainder section */ } while (true);
The compare and swap Instruction
Definition:
int compare_and_swap(int *value, int expected, int new_value) { int temp = *value; if (*value == expected) *value = new_value; return temp; }Properties:
Executed atomically.
Returns the original value of the passed parameter
value.Sets the variable
valueto the value of the passed parameternew_valueonly if*value == expectedis true.
Solution using compareandswap
Shared integer
lockinitialized to 0.Solution:
while (true) { while (compare_and_swap(&lock, 0, 1) != 0) ; /* critical section */ lock = 0; /* remainder section */ }
Bounded-waiting with compare-and-swap
while (true) {
waiting[i] = true;
key = 1;
while (waiting[i] && key == 1)
key = compare_and_swap(&lock,0,1);
waiting[i] = false;
/* critical section */
j = (i + 1) % n;
while ((j != i) && !waiting[j])
j = (j + 1) % n;
if (j == i)
lock = 0;
else
waiting[j] = false;
/* remainder section */
}
Atomic Variables
Instructions like
compare-and-swapare used as building blocks for other synchronization tools.An atomic variable provides atomic (uninterruptible) updates on basic data types like integers and booleans.
Example:
Let
sequencebe an atomic variable.Let
increment()be an operation onsequence.The command
increment(&sequence);ensuressequenceis incremented without interruption.
Atomic Variables
The
increment()function can be implemented as follows:void increment(atomic_int *v) { int temp; do { temp = *v; } while (temp != (compare_and_swap(v,temp,temp+1)); }
Mutex Locks
Previous solutions are complicated and generally inaccessible to application programmers.
OS designers build software tools to solve the critical section problem.
Simplest tool is a mutex lock, a boolean variable indicating if the lock is available or not.
Protect a critical section by:
First
acquire()a lock.Then
release()the lock.
Calls to
acquire()andrelease()must be atomic, usually implemented via hardware atomic instructions such ascompare-and-swap.This solution requires busy waiting and is called a spinlock.
Solution to CS Problem Using Mutex Locks
while (true) {
acquire lock
critical section
release lock
remainder section
}
Semaphore
A synchronization tool that provides more sophisticated ways (than Mutex locks) for processes to synchronize their activities.
Semaphore S: integer variable
Accessed via two indivisible (atomic) operations:
wait()andsignal()(originally called and ).Definition of the
wait()operation:wait(S) { while (S <= 0) ; S--; }Definition of the
signal()operation:signal(S) { S++; }
Semaphore (Cont.)
Counting semaphore: integer value can range over an unrestricted domain.
Binary semaphore: integer value can range only between 0 and 1 (same as a mutex lock).
Can implement a counting semaphore as a binary semaphore.
Semaphores can solve various synchronization problems.
Semaphore Usage Example
Solution to the CS Problem:
c Create a semaphore “mutex” initialized to 1 wait(mutex); CS signal(mutex);Consider and with statements and , respectively, and the requirement that must happen before :
c Create a semaphore “synch” initialized to 0 P1: S1; signal(synch); P2: wait(synch); S2;
Semaphore Implementation
Must guarantee that no two processes can execute the
wait()andsignal()operations on the same semaphore at the same time.The implementation becomes the critical section problem, where the
waitandsignalcode are placed in the critical section.Could have busy waiting in the critical section implementation, but the implementation code is short, resulting in little busy waiting if the critical section is rarely occupied.
Applications may spend lots of time in critical sections, making this implementation less ideal.
Semaphore Implementation with no Busy waiting
Each semaphore has an associated waiting queue.
Each entry in a waiting queue has two data items:
value(of type integer)pointerto the next record in the list
Two operations:
block: place the process invoking the operation on the appropriate waiting queue.wakeup: remove one of the processes in the waiting queue and place it in the ready queue.
Implementation with no Busy waiting (Cont.)
Waiting queue:
typedef struct { int value; struct process *list; } semaphore;
Implementation with no Busy waiting (Cont.)
wait(semaphore *S) {
S->value--;
if (S->value < 0) {
add this process to S->list;
block();
}
}
signal(semaphore *S) {
S->value++;
if (S->value <= 0) {
remove a process P from S->list;
wakeup(P);
}
}
Problems with Semaphores
Incorrect use of semaphore operations:
signal(mutex) …. wait(mutex)wait(mutex) … wait(mutex)Omitting
wait(mutex)and/orsignal(mutex)
These are examples of issues that can occur when semaphores are used incorrectly.
Monitors
A high-level abstraction that provides a convenient and effective mechanism for process synchronization.
Abstract data type with internal variables accessible only by code within the procedure.
Only one process may be active within the monitor at a time.
Pseudocode syntax of a monitor:
monitor monitor-name { // shared variable declarations procedure P1 (…) { … } procedure P2 (…) { … } procedure Pn (…) {……} initialization code (…) { … } }
Schematic View of a Monitor
(Diagram illustrating processes interacting with a monitor, with only one process active inside the monitor at any given time)
Monitor Implementation Using Semaphores
Variables:
semaphore mutex; // (initially = 1)Each procedure is replaced by:
wait(mutex); … body of P; … signal(mutex);Mutual exclusion within a monitor is ensured.
Condition Variables
condition x, y;Two operations allowed on a condition variable:
x.wait(): a process that invokes the operation is suspended untilx.signal().x.signal(): resumes one of the processes (if any) that invokedx.wait(). If nox.wait()on the variable, then it has no effect on the variable.
Monitor with Condition Variables
(Diagram illustrating a monitor with condition variables and processes waiting on these conditions)
Usage of Condition Variable Example
Consider and that need to execute two statements and , respectively, and the requirement that happen before .
Create a monitor with two procedures
F1andF2invoked by and , respectively.One condition variable
xinitialized to 0.One Boolean variable
done.F1:c S1; done = true; x.signal();F2:c if done = false x.wait() S2;
Monitor Implementation Using Semaphores
Variables:
semaphore mutex; // (initially = 1) semaphore next; // (initially = 0) int next_count = 0; // number of processes waiting inside the monitorEach function will be replaced by:
wait(mutex); … body of P; … if (next_count > 0) signal(next) else signal(mutex);Mutual exclusion within a monitor is ensured.
Implementation – Condition Variables
For each condition variable
x, we have:semaphore x_sem; // (initially = 0) int x_count = 0;The operation
x.wait()can be implemented as:x_count++; if (next_count > 0) signal(next); else signal(mutex); wait(x_sem); x_count--;
Implementation (Cont.)
The operation
x.signal()can be implemented as:if (x_count > 0) { next_count++; signal(x_sem); wait(next); next_count--; }
Resuming Processes within a Monitor
If several processes are queued on condition variable
x, andx.signal()is executed, which process should be resumed?FCFS (First-Come, First-Served) is frequently inadequate.
Use the conditional-wait construct of the form
x.wait(c)where:cis an integer (called the priority number).The process with the lowest number (highest priority) is scheduled next.
Single Resource allocation
Allocate a single resource among competing processes using priority numbers that specify the maximum time a process plans to use the resource:
R.acquire(t); … access the resource; … R.release;Where
Ris an instance of typeResourceAllocatorandtis the maximum time.
Single Resource allocation
Allocate a single resource among competing processes using priority numbers that specify the maximum time a process plans to use the resource
The process with the shortest time is allocated the resource first
Let R is an instance of type ResourceAllocator (next slide)
Access to ResourceAllocator is done via:
R.acquire(t); … access the resource; … R.release;Where t is the maximum time a process plans to use the resource
A Monitor to Allocate Single Resource
monitor ResourceAllocator {
boolean busy;
condition x;
void acquire(int time) {
if (busy)
x.wait(time);
busy = true;
}
void release() {
busy = false;
x.signal();
}
initialization code() {
busy = false;
}
}
Single Resource Monitor (Cont.)
Usage:
acquire … releaseIncorrect use of monitor operations:
release() … acquire()acquire() … acquire())Omitting
acquire()and/orrelease()
Liveness
Processes may have to wait indefinitely while trying to acquire a synchronization tool such as a mutex lock or semaphore.
Waiting indefinitely violates the progress and bounded-waiting criteria discussed earlier.
Liveness refers to a set of properties that a system must satisfy to ensure processes make progress.
Indefinite waiting is an example of a liveness failure.
Liveness
Deadlock: two or more processes are waiting indefinitely for an event that can be caused by only one of the waiting processes.
Let and be two semaphores initialized to 1:
P0 P1 wait(S); wait(Q); wait(Q); wait(S); … … signal(S); signal(Q); signal(Q); signal(S);If executes
wait(S)and executeswait(Q), then when executeswait(Q), it must wait until executessignal(Q).However, is waiting until executes
signal(S).Since these
signal()operations will never be executed, and are deadlocked.
Liveness
Other forms of deadlock:
Starvation: indefinite blocking. A process may never be removed from the semaphore queue in which it is suspended.
Priority Inversion: Scheduling problem when a lower-priority process holds a lock needed by a higher-priority process.
Solved via priority-inheritance protocol.