Cooperating processes can either directly share a logical address space(multithread) or be allowed to share data only through files or messages.
Concurrent access to shared data may result in data inconsistency.
6.1 Background
There are two ways to create shared memory between processes:
Method 1: in == out means the buffer is empty, (in + 1) % BUFFER_SIZE == out means buffer is full
BUFFER_SIZE - 1 elements are available
#define BUFFER_SIZE 10
typedef struct {
...
}item;
item buffer[BUFFER_SIZE];
int in = 0;
int out = 0;
// producer process
item nextProduced;
while(true) {
while(((in+1)%BUFFER_SIZE) == out)
; // buffer is full, do nothing
buffer[in] = nextProduced;
in = (in + 1)%BUFFER_SIZE;
}
// consumer process
item nextConsumed;
while(true) {
while(in == out)
; // buffer is empty, do nothing
nextConsumed = buffer[out];
out = (out + 1)%BUFFER_SIZE;
}
Method 2: use an integer variable counter.
BUFFER_SIZE elements are available
// producer
while(true) {
while(counter == BUFFER_SIZE)
; // buffer is full, do nothing
buffer[in] = nextProduced;
in = (in + 1)%BUFFER_SIZE;
counter++;
}
// consumer
while(true) {
while(counter == 0)
; // buffer is empty, do nothing
nextConsumed = buffer[out];
out = (out + 1)%BUFFER_SIZE;
counter--;
}
When we do produce/consume at the same time, the counter value may be incorrect. P 227.
When several processes access and manipulate the same data concurrently and the outcome of the execution depends on the particular order in which the access takes place, it's called race condition.
6.2 The Critical-Section Problem
Each process has a segment of code, called critical section, in which the process may be changing common variables, updating a table, writing a file, and so on.
When one process is executing in its critical section, no other process is to be allowed to execute in its critical section. No two processes are executing in their critical sections at the same time.
The critical-section problem is to design a protocol that the processes can use to cooperate.
Each process must request permission to enter its critical section, the code implementing the request is entry section. Entry section may be followed by anexit section. All other codes areremainder section.
A solution to the critical-section problem must satisfy following requirements:
1. Mutual exclusion. If process P1 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 some processes wish to enter their critical sections, then only those processes that are not executing in their remainder sections can participate in deciding which will enter its critical section next, and the selection cannot be postponed indefinitely.
3. Bounded waiting. There exists a bound, on the number of times that other processes are allowed to enter their critical sections after a process has made a request and before that request is granted.
6.3 Peterson's Solution
Peterson's solution is a classic software-based solution to the critical-section problem.
Peterson's solution is restricted to two processes that alternate execution between their critical sections and remainder sections.
Peterson's solution requires two processes to share two data items:
int turn; // 0 and 1 means the two processes
boolean flag[2]; // flag means if process 0/1 is ready to enter critical section
Implementation of Peterson's solution:
// Structure of process Pi in Peterson's solution
do {
flag[i] = TRUE;
turn = j;
// Pi will waiting if j is ready and turn is on j
while (flag[j] && turn == j);
critical section
flag[i] = FALSE;
remainder section
} while(TRUE);
6.4 Synchronization Hardware
Software-based solutions such as Peterson's are not guaranteed to work on modern computer architectures.
Any solution to the critical-section problem requires a simple tool --- a lock.
Race conditions are prevented by requiring that critical regions be protected by locks. That is, a process must acquire a lock before entering a critical section; it releases the lock when it exits the critical section.
do {
acquire lock
critical section
release lock
remainder section
} while(TRUE);
The critical-section problem could be solved simply in a uniprocessor environment if we could prevent interrupts from occurring while a shared variable was being modified.
Modern computer systems therefore provide special hardware instructions that allow us either to test and modify the content of a word or to swap the contents of two wordsatomically -- that is as one uninterruptable unit.
TestAndSet() instruction:
boolean TestAndSet(boolean *target)
{
boolean rv = *target;
*target = TRUE;
return rv;
}
TestAndSet() can ensure we write to and get the old value atomically. This instruction need hardware support.
wiki:atomicity requires explicit hardware support and hence can't be implemented as a simple function.
Mutual-exclusion Implementation with TestAndSet():
// do not meet bounded waiting
do{
while(TestAndSet(&lock))
; // do nothing
// critical section
lock = FALSE;
// remainder section
} while(TRUE);
Swap() instruction:
void Swap(boolean *a, boolean *b)
{
boolean tmp = *a;
*a = *b;
*b = tmp;
}
Mutual-exclusion Implementation with Swap()
// do not meet bounded-waiting requirement
do {
key = TRUE;
while (key == TRUE)
Swap(&lock, &key);
// critical section
lock = FALSE;
// remainder section
} while(TRUE);
Algorithm that satisfy bounded-waiting requirement:
boolean waiting[n]; // init to false
boolean lock; // init to false
do{
// waiting[i] = TRUE, means Pi want to enter critical section
waiting[i] = TRUE;
key = TRUE;
while(waiting[i] && key)
key = TestAndSet(&lock);
waiting[i] = FALSE;
// critical section
j = (i + 1) % n;
// waiting[j] == FALSE means Pj is not asking to enter critical section,
// so we can keep increase j until we find Pj that want to enter critical section
while((j != i) && !waiting[j])
j = (j + 1)%n;
if(j == i)
lock = FALSE; // In this case, only Pi want to enter, so we reset lock
else
waiting[j] = FALSE; // In this case, another process want to enter, so we set w[j]
// remainder section
} while(TRUE);
Prove:
1. At the beginning, only the first process(Pi) will get lock = FALSE and enter critical section.
2. Once the Pi finish critical section, it will find the next process that want to enter critical section and set lock or w[j] to FALSE.
So each process will wait at most n - 1 turns.
6.5 Semaphores
A semaphore S is an integer variable that, apart from initialization, is accessed only through two standard atomic operations: wait() and signal().
Definition of wait():
wait(S){
while S <= 0
;
S--;
}
Definition of signal();
signal(S) {
S++;
}
All modifications to the integer value of semaphore in the wait() and signal() operations must be executed indivisibly.
6.5.1 Usage
counting semaphore: range over an unrestricted domain
binary semaphore: range only between 0 and 1
On some systems, binary semaphore are known as mutex locks, as they are locks that provide mutual exclusion.
Mutual-exclusion implementation with semaphore:
do {
wait(mutex);
// critical section
signal(mutex);
// remainder section
} while(TRUE);
6.5.2 Implementation
The main disadvantage of the semaphore definition is that it requires busy waiting.
While a process is in its critical section, any other process that tries to enter its critical section must loop continuously in the entry code.
To overcome busy waiting, we can implement block(): it will send process to waiting queue; and wakeup(): it will send process back to ready queue.P 236
typedef struct {
int value; // initial value is 1
struct process *list;
} semaphore;
wait() implementation:
wait(semaphore *S) {
S->value--;
if(S->value < 0) {
add this process to S->list;
block();
}
}
signal() implementation:
signal(semaphore *S) {
S->value++;
if (S->value <= 0) {
remove a process P from S->list; // P is not the process that execute signal!
wakeup(P);
}
}
6.5.3 Deadlocks and Starvation
The implementation of a semaphore with a waiting queue may result in deadlock.
When P0 execute wait(Q), it must wait until P1 executes signal(Q). When P1 execute wait(S), it must wait until P0 executes signal(S).
Another problem related to deadlock is indefinite blocking, orstarvation. Indefinite blocking may occur if we remove processes from the list associated with a semaphore in LIFO.
6.6 Classic Problems of Synchronization
6.6.1 The Bounded-Buffer Problem
To solve bounded-buffer problem, we use mutex semaphore to provide mutual exclusion for access to the buffer pool, it's initialized to 1. The empty and full semaphore count the number of empty and full buffers:
// The structure of the producer process
do {
...
// produce an item in nextp
....
wait(empty); // empty is initialized to n, means n available buffers
wait(mutex); // --mutex, so other processes will be blocked
...
// add nextp to buffer
...
signal(mutex);
signal(full); // full is initialized to 0, means no message
} while(TRUE)
// The structure of consumer process
do {
wait(full);
wait(mutex);
...
// remove an item from buffer to nextc
...
signal(mutex);
signal(empty);
...
// consume the item in nextc
...
} while(TRUE)
6.6.2 The Readers-Writers Problem
One database is accessible from several processes. Processes only read data arereaders, Processes need read and write arewriters.
readers-writers problem: we need to ensure writers have exclusive access to the shared database while writing to the database.
first readers-writers problem: no reader be kept waiting unless a writer has already obtained permission to use the shared object
second readers - writers problem: once a writer is ready, that writer performs its write as soon as possible.
The first problem may cause writer starvation, the second one may cause reader starvation.
Solution to first readers-writers problem:
readers share a data structure
semaphore mutex, wrt; // initialize to 1
int readcount; // init to 0
Structure of writer process:
do {
wait(wrt);
...
// writing is performed
...
signal(wrt);
} while(TRUE);
Structure of reader process:
do {
wait(mutex); // mutex ensure mutual exclusion of update readcount
readcount++;
if (readcount == 1) // the first reader
wait(wrt);
signal(mutex);
...
// reading is performed
...
wait(mutex);
readcount--;
if(readcount == 0)
signal(wrt);
signal(mutex);
} while(TRUE);
When a writer executes signal(wrt), we may resume the execution of either the waiting readers or a single waiting writer. The selection is made by the scheduler.
The readers-writers problem and its solutions have been generalized to providereader-writer locks,the lock is used:
1. In application where it is easy to identify reader/writer
2. In application that have more readers than writers. (The time multiple readers work should compensate the time to create lock)
6.6.3 The Dining-Philosophers Problem
Five philosophers are sitting around a table, there are five chopsticks. One must get two chopsticks to eat.
First, we need to create an array of semaphore:
semaphore chopstick[5]; // init to 1
The structure of philosopher i:
do {
wait(chopstick[i]);
wait(chopstick[(i + 1)%5]);
...
// eat
...
signal(chopstick[i]);
signal(chopstick[(i + 1)%5]);
...
// think
} while(TRUE);
To avoid deadlock:
1. Allow at most four philosophers to be sitting simultaneously at the table.
2. Allow a philosopher to pick up her chopsticks only if both chopsticks are available (to do this, she must pick them up in a critical section).
3. Use an asymmetric solution; that is, an odd philosopher picks up first her left chopstick and then her right chopstick, whereas an even philosopher picks up her right chopstick and then her left chopstick.
We can give different priority to prevent starvation!