05-Synchronization

Operating Systems Lecture Notes
Synchronization
Matthew Dailey
Some material © Silberschatz, Galvin, and Gagne, 2002
Synchronization
WHAT: To synchronize is to make things happen at the same time.
HOW: By making one process wait for another.
This guarantees that certain points in each process occur at the
same time.
WHY: — Ensure consistency of shared data (prevent race conditions)
— Make processes wait for resources to become available
This is arguably the most important topic in the course!
Readings: Silberschatz et al., chapter 7
Why do we need to synchronize?
EXAMPLE: Bounded buffer problem with shared memory
Producer:
Consumer:
while ( 1 ) {
/* produce item -> nextProduced */
while ( counter == BUFFER_SIZE )
; /* do nothing */
buffer[in] = nextProduced;
in = ( in + 1 ) % BUFFER_SIZE;
counter++;
}
While ( 1 ) {
while ( counter == 0 )
; /* do nothing */
nextConsumed = buffer[out];
out = ( out + 1 ) % BUFFER_SIZE;
counter--;
/* consume item -> nextConsumed */
Example continued
Producer and consumer code is correct if they do not run concurrently.
But with concurrency, the answer could be incorrect!
Suppose the C statement “counter++” is implemented in assembly as
register1 = counter
register1 = register1 + 1
counter = register1
Suppose the C statement “counter--” is implemented in assembly as
register2 = counter
register2 = register2 - 1
counter = register2
Example continued: interleaved execution
If the producer and consumer run concurrently, execution can be
interleaved:
T0:
T1:
T2:
T3:
T4:
T5:
producer executes register1 = counter
producer executes register1 = register1 + 1
consumer executesregister2 = counter
consumer executesregister2 = register2 - 1
producer executes counter = register1
consumer executescounter = register2
(reg1 = 5)
(reg1 = 6)
(reg2 = 5)
(reg2 = 4)
(counter = 6)
(counter = 4)
But the correct answer at the end should be counter = 5!!
Race Conditions
If we swapped T4 and T5 in the previous interleaving:
– Result would be counter = 6
– The outcome depends on the order of execution!
A race condition is when the outcome depends on the particular order in which
instructions execute.
To guard against race conditions we must synchronize the processes/threads
This is not a toy example! It happens all the time in complex multitasking systems.
Critical Section Problem
Suppose we have n processes, P0, P1, …, Pn-1.
Each process has a special segment of code
– Called the critical section
– That updates shared data
GOAL: design a protocol such that when one process is
executing in its critical section, no other process is allowed into
its critical section.
Another way of saying it:
– Critical section execution must be mutually exclusive in time.
Let’s look at the code again
Producer:
Consumer:
while ( 1 ) {
/* produce item,
then put in nextProduced */
... ... ...
while ( counter == BUFFER_SIZE )
; /* do nothing */
buffer[in] = nextProduced;
in = ( in + 1 ) % BUFFER_SIZE;
counter++;
}
while ( 1 ) {
while ( counter == 0 )
; /* do nothing */
nextConsumed = buffer[out];
out = ( out + 1 ) % BUFFER_SIZE;
counter--;
/* consume item in nextConsumed */
... ... ...
}
Where are the critical sections?
• We have shown that counter needs to be protected in a critical section.
• With careful analysis, you will see that buffer does not need protection.
Let’s look at the code again
Producer:
Consumer:
while ( 1 ) {
/* produce item,
then put in nextProduced */
... ... ...
while ( counter == BUFFER_SIZE )
; /* do nothing */
buffer[in] = nextProduced;
in = ( in + 1 ) % BUFFER_SIZE;
counter++;
}
while ( 1 ) {
while ( counter == 0 )
; /* do nothing */
nextConsumed = buffer[out];
out = ( out + 1 ) % BUFFER_SIZE;
counter--;
/* consume item in nextConsumed */
... ... ...
}
If the critical section execution are mutually exclusive in time, then the
interleaving of the counter updates will not happen. Then, the answer
will always be correct.
Critical Section Problem
Assume the following structure for every process Pi:
While ( not done ) {
Remainder section
Entry section
Critical section
Exit section
Remainder section
}
Entry section is a barrier. It
blocks processes out when one
process is in its critical section.
Exit releases the barrier. It no
longer blocks other processes
out of their critical sections.
Critical Section [CS] Problem
The critical section problem: How to protect a critical section?
Solutions to the CS problem must satisfy:
– Mutual Exclusion:
When process I (Pi) is executing its CS, no other process can execute in its CS.
– Progress:
No process in the remainder section is allowed to block processes wanting to
enter their CS.
– Bounded waiting:
Must be able to guarantee that, once Pi requests entry to its CS, no more than k
other processes will be allowed to enter before Pi does.
We assume nothing about relative speed of the n processes.
Solution 1: Take Turns?
Uses a shared integer variable named turn
Code for P0:
Code for P1:
ENTRY
ENTRY
while ( turn != 0 );
while ( turn != 1 ) ;
/* Critical section */
/* Critical section */
EXIT
EXIT
turn = 1;
turn = 0;
Solution 1: Does it work?
Mutual exclusion


– Neither of the processes can enter until it is their “turn”
– The other process only changes “turn” when done with CS
Bounded waiting
– If P0 has to wait, P1 enters its CS at most once before P0 gets its chance
Progress

– If turn == 1 but P1 is in its remainder section, P0 cannot enter its CS!
Solution 2: State intention?
Uses an array of booleans, flag[]
Code for P0:
Code for P1:
ENTRY
ENTRY
flag[0] = TRUE;
while (flag[1] == TRUE) ;
flag[1] = TRUE;
while (flag[0] == TRUE) ;
/* Critical section */
/* Critical section */
EXIT
EXIT
flag[0] = FALSE;
flag[1] = FALSE;
Solution 2: Does it work?
Mutual exclusion

– If P0 is in CS, then flag[0] == TRUE
– P1 will never enter CS if flag[0] == TRUE
Bounded waiting


– Suppose P0 sets flag[0] == TRUE
– Then P1 sets flag[1] to TRUE
– Both P0 and P1 will wait indefinitely
Progress
– Same as above
Solution 3: Combination of 1 & 2
Uses int turn=0; AND boolean flag[2] = {FALSE,FALSE};
Code for P0:
Code for P1:
ENTRY
ENTRY
flag[0] = TRUE;
turn = 1;
while (flag[1] && turn == 1) ;
flag[1] = TRUE;
turn = 0;
while (flag[0] && turn == 0) ;
/* Critical section */
/* Critical section */
EXIT
EXIT
flag[0] = FALSE;
flag[1] = FALSE;
Proof of Mutual Exclusion in Solution 3
Assume towards a contradiction that P0 and P1 are in CS simultaneously.
Since P0 and P1 are both in CS, flag = { TRUE, TRUE }.
Without loss of generality, assume P1 entered CS at some time t, while P0
was already in its CS.
Since flag[0]= TRUE at time t, it must be the case that turn=1 at
time t (otherwise P1 could not enter CS).
This implies P0 set turn=1 after P1 set turn=0.
But if that is true, then P0 would have to wait at the while loop.
This means P1 entered its CS before P0 did… contradiction!
Mutual exclusion follows. P0 and P1 cannot be in CS simultaneously.
(See text for proof of bounded wait and progress)
Other ways to get mutual exclusion
For more than 2 processes, the CS problem is harder.
The hardware can help us achieve mutual exclusion.
Idea 1: disable interrupts during the CS
– Simple and easy to implement
– Restrictive: No other process in the entire system can run
– User processes can have a very long CS
Better idea: an atomic test-and-set instruction



Test-and-set instruction
Pseudocode:
boolean TestAndSet( boolean *pTarget ) {
boolean rv = *pTarget;
*pTarget = TRUE;
return rv;
}
Basic idea: runs the entire procedure atomically (i.e. disallow interrupts)
Can use this to implement a lock for mutual exclusion
Mutual Exclusion with TestAndSet
Uses boolean locked; AND TestAndSet function
Code for P0:
Code for P1:
ENTRY
ENTRY
while (TestAndSet(&locked)) ;
while (TestAndSet(&locked)) ;
/* Critical section */
/* Critical section */
EXIT
EXIT
locked = FALSE;
A lock is also known as a mutex.
locked = FALSE;
Semaphores
Definition: A semaphore is an abstract data type with operations allowing
mutual exclusion.
A semaphore S is just an integer that can only be modified by three
operations: init(S,i), wait(S), and signal(S)
init(S,i) {
S = i;
}
wait(S) {
while( S <= 0 );
S--;
}
signal(S) {
S++;
}
If the semaphore’s value is 0 when wait is called, we have to wait for
another process to signal the semaphore (incrementing the value to 1).
Then we will be able to decrement the value to 0 again and enter our
critical section.
Mutual Exclusion with Semaphores
Uses a semaphore, semaphore mutex = 1;
Code for P0:
Code for P1:
ENTRY
ENTRY
wait(mutex);
wait(mutex);
/* Critical section */
/* Critical section */
EXIT
EXIT
signal(mutex);
signal(mutex);
More on Semaphores
Semaphores are the most common (thus most important) synchronization
primitive in used in modern multithreaded applications.
You will probably use semaphores in many projects in your future careers!
Semaphore Implementation
The busy-wait in the wait(S) operation is inefficient.
Also, to ensure bounded waiting and progress, we need to enforce an
ordering on the set of waiting processes.
So the implementation of wait(S) actually blocks the calling process
and puts it on a queue of processes waiting for S.
The implementation of signal(S) unblocks the first waiting process at
the head of the queue.
So wait() and signal() therefore have their own critical sections.
Typically the OS briefly disables interrupts during the critical sections of
wait() and signal().
Monitors
Semaphores: a low-level synchronization structure
– Widely used for many synchronization tasks
– Can be used to solve the critical section problem if wait() and
signal() calls are used correctly
– But error-prone. What if I forget to put wait() and signal() around
my critical section?
Monitors: a higher-level synchronization structure
– Solves the critical section problem automatically!
– Can be used for other synchronization tasks too.
What is a Monitor?
Like a C++ class, or like a C struct that can only be modified by particular
functions.
A monitor has:
– Some private data
– Functions that manipulate that private data
Access rules for monitors:
– Processes cannot directly access internal monitor data.
– Monitor functions cannot access external data.
Monitor Syntax
monitor monitor-name {
shared variable declarations
procedure body P1 ( . . . ) {
. . .
}
procedure body P2 ( . . . ) {
. . .
}
. . .
{
initialization code
}
}
Monitor Semantics
Only one process at a time can be active within a monitor. This means an entry
queue is required.
Additional variable type: the condition variable. Declared as
condition x, y;
Condition variables have two operations:
– x.wait() : suspends calling process until some other process invokes x.signal()
– x.signal(): resumes one (and only one) suspended process. Has no effect if there
are no suspended processes.
Monitor Schematic
Monitor With Condition Variables
Monitor Implementation Issue
What exactly should happen when P calls x.signal() while Q is
waiting on x?
We said only one process can be active in a monitor at one time, so we
have to enforce either of the following:
– P waits until Q either leaves the monitor or Q waits for another condition
– Q waits until P either leaves the monitor or P waits for another condition
Different systems may implement x.signal() either way.
Solving Dining Philosophers With a Monitor
monitor dp {
enum { thinking, hungry, eating } state[5];
condition self[5];
void pickup(int i);
void putdown(int i);
void test(int i);
void init() {
for ( int i=0; i<5; i++) {
state[I] = thinking;
}
}
}
Solving Dining Philosophers With a Monitor
void pickup(int i) {
state[i] = hungry;
test(i);
if (state[i] != eating)
self[i].wait();
}
}
void putdown(int i) {
state[i] - thinking;
test((i+4)%5);
test((i+1)%5);
}
void test(int i) {
if ((state[(i+4)%5] != eating) &&
(state[i] == hungry) &&
(state[(i+1)%5] != eating)) {
state[i] = eating;
self[i].signal();
}
}
/* Each philosopher thread/process
does: */
dp.pickup(i);
…
eat
…
dp.putdown(i);
Dining Philosophers Example
Try the following sequence with the monitor solution:
–
–
–
–
T0: Philosopher 0 gets hungry
T1: Philosopher 3 gets hungry
T2: Philosopher 1 gets hungry
T3: Philosopher 0 finished eating
What is the state of each philosopher thread and each condition variable at
time T4?
What have we learned?
How race conditions can lead to incorrect results.
The critical section (CS) problem:
– Requirements for solutions
– Not-quite-correct software solutions
– A correct software solution to the 2-process CS problem
– A hardware-supported solution to the CS problem
Mutual exclusion as an important goal:
– Solutions: TestAndSet, Semaphores, Monitors
[Monitors can do much more!]