Chapter 5
Chapter 5
Process Synchronization
outline
• Background
• The Critical-Section Problem
• Synchronization Hardware
• Semaphores
• Classical Problems of Synchronization
• Critical Regions
• Monitors
• Atomic Transactions
Background
• Concurrent access to shared data may result in data inconsistency.
• Maintaining data consistency requires mechanisms to ensure the
orderly execution of cooperating processes.
• Suppose that we modify the producer-consumer code by adding a
variable counter, initialized to 0 and incremented each time a new item
is added to the buffer.
• The new scheme is illustrated by the following:
Cont’d
• Shared data
typedef .... item;
item buffer[N];
int in, out, counter;
in = 0;
out = 0;
counter = 0;
Cont’d
• Producer process
while(true) {
...
produce an item in nextp
...
while(counter == n)
no-op;
buffer[in] = nextp;
in = (in+1)%n;
counter = counter + 1;
}
Cont’d
• Consumer process
while(true) {
while(counter == 0)
no-op;
nextc = buffer[out];
out = (out+1)%n;
counter = counter - 1;
...
consume the item in nextc
...
}
• The statements:
counter = counter + 1;
counter = counter - 1;
• must be xecuted atomically.
The Critical-Section Problem
• n processes all competing to use some shared data
• Each process has a section of code code, called its critical section, in
which the shared data is accessed.
• Problem - ensure that when one process is executing in its critical
section, no other process is allowed to execute in its critical section.
• Structure of process Pi
while(true) {
entry section
critical section
exit section
remainder section
}
Cont’d
• A solution to the critical-section problem must satisfy the
following three requirements:
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 are some processes that wish to enter their critical
section, then the selection of the processes 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.
• Assumption that each process is executing at a nonzero speed.
• No assumption concerning relative speed of the n processes.
Cont’d
• Initial attempts to solve the problem.
• Only 2 processes, P0 and P1
• General structure of process Pi (other process Pj )
while(true) {
entry section
critical section
exit section
remainder section
}
• Processes may share some common variables to synchronize their
actions
Algorithm 1
• Shared variables:
int turn;
initially turn = 0
turn = i -> Pi can enter its critical section
• Process Pi
while(true) {
while(turn!=i) no-op;
critical section
turn = j;
remainder section
}
• Satisfies mutual exclusion, but not progress.
Algorithm 2
• Shared variables
bool flag[2];
initially flag[0] = flag[1] = false.
flag[i] = true -> Pi ready to enter its critical section
• Process Pi
while(true) {
flag[i] = true;
while(flag[j]) no-op;
critical section
flag[i] = false;
remainder section
until false;
• Does not satisfy progress because:
• If the two processes set their flags to true at the same time, then they
will both wait forever.
Algorithm 3
• Shared data:
bool lock=false;
Process Pi
bool key;
while(true) {
key = true;
do {
Exchg(lock,key);
}while(key);
critical section
lock = false;
remainder section
}
Semaphore - synchronization tool that does not
require busy waiting
• Semaphore S
• integer variable introduced by Dijkstra can only be accessed via two
indivisible (atomic) operations
wait(S): S = S - 1; if S < 0 then block(S)
signal(S): S = S + 1; if S <= 0 then wakeup(S)
• sometimes wait and signal are called down and up or P and V
• block(S) - results in suspension of the process invoking it (sometimes
called sleep).
• wakeup(S) - results in resumption of exactly one process that has
invoked block(S)
Cont’d
• Example: critical section for n processes
• Shared variables
semaphore mutex=1;
Process Pi
while(true) {
wait(mutex);
critical section
signal(mutex);
remainder section }
• Implementation of the wait and signal operations so that they must
execute atomically.
• Uniprocessor:
• Disable interrupts around the code segment implementing the wait and signal
operations.
• Multiprocessor:
• If no special hardware provided, use a correct software solution to the critical-
section problem, where the critical sections consist of the wait and signal
operations.
• Use special hardware if available, i.e., TestandSet:
Implementation of wait(S) operation with the TestandSet instruction:
• Shared variables
boolean lock = false;
• Code for wait(S)
while (TestandSet(lock));
S = S - 1;
if (S < 0) {
lock = false;
block(S);
} else
lock = false;
• Code for signal(S)
while (TestandSet(lock));
S = S + 1;
if (S 0)
wakeup(S);
lock = false;
• Race condition exists!
Cont’d
• Better Code for wait(S)
while (TestandSet(lock1));
while (TestandSet(lock));
S = S - 1;
if (S < 0) {
lock = false;
block(S);
} else
lock = false;
lock1 = false;
lock1 serialises the waits.
• Semaphore can be used as general synchronization tool:
• Execute B in Pj only after A executed in Pi
• Use semaphore flag initialized to 0
• Code:
Pi Pj
-- --
. .
. .
. .
A wait(flag)
signal(flag) B
Cont’d
• Shared data
int p[N]; /* status of the philosophers */
semaphore s[N]=0; /* semaphore for each philosopher */
semaphore mutex=1; /* semaphore for mutual exclusion */
• Code
#define LEFT(n) (n+N-1)%N /* Macros to give left */
#define RIGHT(n) (n+1)%N /* and right around the table */
void test(int no) { /* can philosopher 'no' eat */
if ((p[no] == HUNGRY) &&
(p[LEFT(no)] != EATING) &&
(p[RIGHT(no)] != EATING) ) {
p[no]=EATING;
signal(s[no]); /* if so then eat */
}
}
Cont’d
void take_forks(int no) { /* get both forks */
wait(mutex); /* only one at a time here please */
p[no]=HUNGRY; /* I'm Hungry */
test(no); /* can I eat? */
signal(mutex);
wait(s[no]); /* wait until I can */ }
void put_forks(int no) { /* put the forks down */
wait(mutex); /* only one at a time here */
p[no]=THINKING; /* let me think */
test(LEFT(no)); /* see if my neighbours can now eat */
test(RIGHT(no));
signal(mutex); }
void philosopher(int no) {
while(1) {
...think....
take_forks(no); /* get the forks */
....eat.....
put_forks(no); /* put forks down */
}
return NULL;}
High-level synchronization constructs
Monitors
• High-level synchronization construct that allows the safe sharing of an abstract
data type among concurrent processes. (Hoare and Brinch Hansen 1974)
• A collection of procedures, variables and data structures. Only one process can
be activein a monitor at any instant.
• monitor example
integer i;
condition c;
procedure producer(x);
begin
.
end
procedure consumer(x);
begin
.
end
end monitor;
Cont’d
• monitor ProducerConsumer
condition full, empty;
integer count;
procedure enter;
begin
if count = N then wait(full);
...enter item...
count := count + 1;
if count = 1 then signal(empty)
end;
procedure remove;
begin
if count = 0 then wait(empty);
...remove item...
count := count - 1;
if count = N - 1 then signal(full)
end;
count := 0;
end monitor;
Cont’d
• procedure producer;
begin
while true do
begin
...produce item...
ProducerConsumer.enter
end
end;
procedure consumer;
begin
while true do
begin
ProducerConsumer.remove;
...consume item...
end
end;
The dining philosophers problem can also be solved easily
• monitor dining-philosophers
status state[n];
condition self[n];
procedure pickup (i:integer);
begin
state[i] := hungry;
test (i);
if state[i] <> eating then wait(self[i]);
end;
procedure putdown (i:integer);
begin
state[i] := thinking;
test (i+4 mod 5);
test (i+1 mod 5);
end;
Cont’d
• procedure test (k:integer);
begin
if state[k+4 mod 5] <> eating
and state[k] = hungry
and state[k+1 mod 5] <> eating
then begin
state[k] := eating;
signal(self[k]);
end;
end;
begin
for i := 0 to 4
do state[i] := thinking;
end
end monitor
Cont’d
• procedure philosopher(no:integer);
begin
while true do
begin
...think....
pickup(no);
....eat.....
putdown(no)
end
end
• There are very few languages that support constructs such as monitors...
expect this to change. One language that does is Java. Here is a Java
class that can be used to solve the producer consumer problem.
Cont’d
class CubbyHole {
private int seq;
private boolean available = false;
public synchronized int get() {
while (available == false) {
try {
wait();
} catch (InterruptedException e) {} }
available = false;
notify();
return seq; }
public synchronized void put(int value) {
while (available == true) {
try {
wait();
} catch (InterruptedException e) {} }
seq = value;
available = true;
notify(); } }
Monitor implementation using semaphores
1. Output all log records currently residing in volatile storage onto stable
storage.
2. Output all modified data residing in volatile storage to stable storage.
3. Output log record <checkpoint> onto stable storage.
• Recovery routine examines log to determine the most recent transaction
Ti that started executing before the most recent checkpoint took place.
• Search log backward for first <checkpoint> record.
• Find subsequent <Ti start> record.
• redo and undo operations need to be applied to only transaction Ti and
all transactions Tj that started executing after transaction Ti .
Concurrent Atomic Transactions