ch6 EN BK Syn1
ch6 EN BK Syn1
Background
Monitors
Liveness
Evaluation
Illustrate hardware solutions to the critical-section problem using memory barriers, compare-
and-swap operations, and atomic variables
Demonstrate how mutex locks, semaphores, monitors, and condition variables can be used to
solve the critical-section problem
Evaluate tools that solve the critical-section problem in low-, moderate-, and high-contention
scenarios
while (true) {
; /* do nothing */
buffer[in] = next_produced;
counter++;
while (true) {
while (counter == 0)
; /* do nothing */
next_consumed = buffer[out];
counter--;
Processes P0 and P1 are creating child processes using the fork() system call
Unless there is
mutual exclusion, the
same pid could be
assigned to two
different processes!
} while(true);
1. Mutual Exclusion – If process Pi is executing in its critical section, then no other processes
can be executing in their critical sections
2. Progress – If no process is executing in its critical section and there exist some processes
that wish to enter their critical section, then the selection of process that will enter the critical
section next cannot be postponed indefinitely
3. Bounded Waiting – A bound must exist on the number of times that other processes are
allowed to enter their critical sections after a process has made a request to enter its critical
section and before that request is granted
Assume that each process executes at a nonzero speed
Non-preemptive – runs until exits kernel mode, blocks, or voluntarily yields CPU
Shared variable
• int turn; /* initialize turn = 0 */
• If turn = i then Pi is permitted to enter CS
Process Pi
do {
while (turn != i);
critical section
turn = j;
remainder section
} while (1);
Process P0 Process P1
do { do {
while (turn != 0); while (turn != 1);
critical section critical section
turn := 1; turn := 0;
remainder section remainder section
} while (1); } while (1);
• Achievemutual exclusion
(1),
• Violate condition of
progress (2).
Operating System Concepts 15 Silberschatz, Galvin and Gagne ©2018
Proposal solution 2 (1/2)
Shared variable
• boolean flag[ 2 ]; /* initialize flag[0] = flag[1] = false */
• flag[i] = true notice that Pi want to enter CS
Process Pi
do {
flag[ i ] = true;
while (flag[ j ]);
critical section
flag[ i ] = false;
remainder section
} while (1);
§ Process P0 § Process P1
do { do {
flag[ 0 ] = true; flag[ 1 ] = true;
while (flag[ 1 ]); while (flag[ 0 ]);
critical section critical section
flag[ 0 ] = false; flag[ 1 ] = false;
remainder section remainder section
} while (1); } while (1);
Achievemutual exclusion (1),
Violate condition of progress (2).
Two-processes solution
Assume that the load and store machine-language instructions are atomic; that is, it cannot
be interrupted
The two processes share two variables:
int turn;
boolean flag[i]
4 The variable turn indicates whose turn it is to enter the critical section
4 The flag[] array is used to indicate if a process is ready to enter the critical section
– flag[i] = true implies that process Pi is ready!
while (true){
flag[i] = true;
turn = j;
; /* do nothing */
/* critical section */
flag[i] = false;
/* remainder section */
Although useful for demonstrating an algorithm, Peterson’s Solution is not guaranteed to work
on modern architectures
Understanding why it will not work is also useful for better understanding race conditions
To improve performance, processors and/or compilers may reorder operations that have no
dependencies
For single-threaded, this is ok as the result will always be the same.
Thread 1 performs
while (!flag)
;
print x
Thread 2 performs
x = 100;
flag = true
flag = true;
x = 100;
This allows both processes to be in their critical section at the same time!
Many systems provide hardware support for implementing the critical-section code.
1. Memory barriers
2. Hardware instructions
3. Atomic variables
Memory model is the memory guarantee that a computer architecture makes to application
programs.
Memory models may be either:
Ø Strongly ordered – where a memory modification of one processor is immediately visible to all other
processors.
Ø Weakly ordered – where a memory modification of one processor may not be immediately visible to
all other processors.
A memory barrier is an instruction that forces any change in memory to be propagated (made
visible) to all other processors.
We could add a memory barrier to the following instructions to ensure Thread 1 outputs 100:
while (!flag)
memory_barrier();
print x;
Thread 2 now performs
x = 100;
memory_barrier();
flag = true;
Special hardware instructions that allow us to either test-and-modify the content of a word, or
to swap the contents of two words atomically (uninterruptedly.)
Test-and-Set() instruction
Compare-and-Swap() instruction
Definition:
boolean test_and_set(boolean *target)
{
boolean rv = *target;
*target = true;
return rv:
}
1. Executed atomically
2. Returns the original value of passed parameter (i.e., *target)
3. Set the new value of passed parameter to true
(i.e., *target=true)
Solution:
do {
while (test_and_set(&lock))
; /* do nothing */
/* critical section */
lock = false;
/* remainder section */
} while (true);
Definition:
int compare_and_swap(int *value, int expected,
int new_value) {
int temp = *value;
if (*value == expected)
*value = new_value;
return temp;
}
1. Executed atomically
2. Returns the original value of passed parameter value
3. Set the variable value the value of the passed parameter new_value but only if *value
== expected is true. That is, the swap takes place only under this condition.
Solution:
while (true){
while (compare_and_swap(&lock, 0, 1) != 0)
; /* do nothing */
/* critical section */
lock = 0;
/* remainder section */
}
Operating System Concepts 31 Silberschatz, Galvin and Gagne ©2018
Bounded-waiting Mutual Exclusion
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 */
}
Operating System Concepts 32 Silberschatz, Galvin and Gagne ©2018
Atomic Variables
Typically, instructions such as compare-and-swap are used as building blocks for other
synchronization tools.
One tool is an atomic variable that provides atomic (uninterruptible) updates on basic data
types such as Integers and Booleans.
For example, the increment() operation on the atomic variable sequence ensures
sequence is incremented without interruption:
increment(&sequence);
do {
temp = *v;
}
while
(temp != compare_and_swap(v,temp,temp+1));
Protect a critical section by first acquire() a lock then release() the lock
while (true) {
acquire lock;
critical section;
release lock;
remainder section;
4 acquire() {
while (!available)
; /* busy wait */
available = false;;
}
4 release() {
available = true;
}
These two functions must be implemented atomically
Two operations:
block – place the process invoking the operation on the appropriate waiting queue
wakeup – remove one of processes in the waiting queue and place it in the ready queue
typedef struct {
int value;
struct process *list;
} semaphore;
Must guarantee that no two processes can execute the wait() and signal() on the same
semaphore at the same time
Thus, the implementation becomes the critical-section problem where the wait() and
signal() code are placed in the critical section
Could now have busy waiting in critical-section implementation
4 But implementation code is short
Note that applications may spend lots of time in critical sections and therefore this is not a
good solution
wait(S1); wait(S2);
signal(S2); signal(S1);
} }
signal(mutex) …. wait(mutex)
wait(mutex) … wait(mutex)
These – and others – are examples of what can occur when semaphores and other
synchronization tools are used incorrectly.
A high-level abstraction that provides a convenient and effective mechanism for process
synchronization
Abstract data type, internal variables only accessible by code within the procedure
condition x, y;
Options include
Signal and wait – P waits until Q either leaves the monitor or it waits for another condition
Signal and continue – Q waits until P either leaves the monitor or it waits for another condition
wait(not_empty);
*x = get resource from array “resources” signal(not_full);
}
Operating System Concepts 52 52 Galvin and Gagne ©2018
Silberschatz,
Producer-Consumer with Monitors (Mesa)
Monitor bounded_buffer {
buffer resources[N];
condition not_full, not_empty;
produce(resource x) {
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 resurce;
...
R.release;
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;
}
}
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 at the
beginning of this chapter
Liveness refers to a set of properties that a system must satisfy to ensure processes make
progress
Deadlock – two or more processes are waiting indefinitely for an event that can be caused by
only one of the waiting processes
Let S and Q be two semaphores initialized to 1
P0 P1
wait(S); wait(Q);
wait(Q); wait(S);
... ...
signal(S); signal(Q);
signal(Q); signal(S);
Consider if P0 executes wait(S) and P1 wait(Q). When P0 executes wait(Q), it must wait
until P1 executes signal(Q)
However, P1 is waiting until P0 execute signal(S)
A process may never be removed from the semaphore queue in which it is suspended
Priority Inversion – Scheduling problem when lower-priority process holds a lock needed by
higher-priority process
Solved via priority-inheritance protocol
Consider the scenario with three processes P1, P2, and P3.
P1 has the highest priority, P2 the next highest, and P3 the lowest.
Assume a resource P3 is assigned a resource R that P1 wants. Thus, P1 must wait for P3 to finish
using the resource.
This simply allows the priority of the highest thread waiting to access a shared resource to be
assigned to the thread currently using the resource.
Thus, the current owner of the resource is assigned the priority of the highest priority thread wishing
to acquire the resource.
A race condition occurs when processes have concurrent access to shared data and the final
result depends on the particular order in which concurrent accesses occur. Race conditions
can result in corrupted values of shared data.
A critical section is a section of code where shared data may be manipulated and a possible
race condition may occur. The critical-section problem is to design a protocol whereby
processes can synchronize their activity to cooperatively share data.
A solution to the critical-section problem must satisfy the following three requirements: (1)
mutual exclusion, (2) progress, and (3) bounded waiting. Mutual exclusion ensures that only
one process at a time is active in its critical section. Progress ensures that programs will
cooperatively determine what process will next enter its critical section. Bounded waiting limits
how much time a program will wait before it can enter its critical section.
Software solutions to the critical-section problem, such as Peterson’s solution, do not work
well on modern computer architectures.
Hardware support for the critical-section problem includes memory barriers; hardware
instructions, such as the compare-and-swap instruction; and atomic variables.
A mutex lock provides mutual exclusion by requiring that a process acquire a lock before
entering a critical section and release the lock on exiting the critical section.
Semaphores, like mutex locks, can be used to provide mutual exclusion. However, whereas a
mutex lock has a binary value that indicates if the lock is available or not, a semaphore has an
integer value and can therefore be used to solve a variety of synchronization problems.
A monitor is an abstract data type that provides a high-level form of process synchronization.
A monitor uses condition variables that allow processes to wait for certain conditions to
become true and to signal one another when conditions have been set to true.
Solutions to the critical-section problem may suffer from liveness problems, including
deadlock.
The various tools that can be used to solve the critical-section problem as well as to
synchronize the activity of processes can be evaluated under varying levels of contention.
Some tools work better under certain contention loads than others.