• Problem description

• There is a shared FIFO buffer of a certain fixed size

• One thread writes into the shared buffer

The writer thread must block (wait) when the shared buffer is full

The reader thread must block (wait) when the shared buffer is empty

• The reader/writer problem uses a circular buffer to store the data items:

 ``` nItems = 0 buf: 0 1 2 3 4 ... N-1 +---+---+---+---+---+---+---+---+---+---+---+ | | | | | | | | | | | | +---+---+---+---+---+---+---+---+---+---+---+ ```

• Addition variables (besides the buffer):

 nItems = # items in the buffer A read pointer that points to the next item that is read A write pointer that points to the next empty slot in the buffer

The pointers are initialized as follows:

 ``` nItems = 0 buf: 0 1 2 3 4 ... N-1 +---+---+---+---+---+---+---+---+---+---+---+ | | | | | | | | | | | | +---+---+---+---+---+---+---+---+---+---+---+ ^ | readPtr writePtr ```

• After the writer write an item into the buffer, the writePtr is advanced:

 ``` nItems = 1 buf: 0 1 2 3 4 ... N-1 +---+---+---+---+---+---+---+---+---+---+---+ | x | | | | | | | | | | | +---+---+---+---+---+---+---+---+---+---+---+ ^ ^ | | readPtr | | writePtr ```

• After 3 more writes, the writePtr is then advanced to:

 ``` nItems = 4 buf: 0 1 2 3 4 ... N-1 +---+---+---+---+---+---+---+---+---+---+---+ | x | x | x | x | | | | | | | | +---+---+---+---+---+---+---+---+---+---+---+ ^ ^ | | readPtr | | writePtr ```

(Notice that the writePtr always points to the next empty slot.

 ``` nItems = 3 buf: 0 1 2 3 4 ... N-1 +---+---+---+---+---+---+---+---+---+---+---+ | | x | x | x | | | | | | | | +---+---+---+---+---+---+---+---+---+---+---+ ^ ^ | | readPtr | | writePtr ```

• Solving the problem with locks

• The need for locks

 The reader and the writer must access the shared variable nItems (Note: the read pointer and the write pointer are not shared)

• Consider the follwoing solution using mutex locks:

 ``` Shared variables: int MAX; int nItems = 0; // Number of items in buffer int buf[MAX]; Writer: while ( true ) { int x; x = produce_next_value(); lock(mutex); if ( nItems == N ) WAIT (BLOCK); // When BLOCKED, we need some // OTHER process to unblock it ! buf[writePtr] = x; // Put x in buffer writePtr = (writePtr+1) % N; nItems++; if ( reader is BLOCKED ) UNBLOCK reader; unlock(mutex); } Reader: while ( true ) { int x; lock(mutex); if ( nItems == 0 ) WAIT (BLOCK); // When BLOCKED, we need some // OTHER process to unblock it ! x = buf[readPtr++]; // Get x from buffer readPtr = (readPtr+1) % N; nItems--; if ( writer is BLOCKED ) UNBLOCK writer; unlock(mutex); consume_value(x); } ```

• This solution will end in deadlock

• Here is a sequence of steps that will leads to deadlock:

 ``` 1. Reader starts first 2. Reader locks the mutex 3. Reader finds buffer empty 4. Reader blocks 5. Writer starts 6. Writer tries to lock the mutex 7. Write blocks too ```

The system is now dead locked because the writer cannot put an item in the buffer to release the reader

• A solution without locks

• We can avoid using locks by eliminating the shared variable nItems.

• We can eliminate nItems by noting that we can compute the number of items using the read and write locations:

• The buffer is empty when readPtr == writePtr:

 ``` nItems = 0 buf: 0 1 2 3 4 ... N-1 +---+---+---+---+---+---+---+---+---+---+---+ | | | | | | | | | | | | +---+---+---+---+---+---+---+---+---+---+---+ ^ | readPtr writePtr ```

• The difference:

 ``` [ (writePtr + N) - readPtr ] % N ```

between writePtr and readPtr is the number of items in the buffer

Case 1:

 ``` nItems = 4 buf: 0 1 2 3 4 5 6 7 8 9 10 +---+---+---+---+---+---+---+---+---+---+---+ | | x | x | x | x | | | | | | | +---+---+---+---+---+---+---+---+---+---+---+ ^ ^ | | readPtr | | writePtr ((writePtr + 11) - readPtr) % 11 = ((5 + 11) - 1) % 11 = (16 - 1) % 11 = 15 % 11 = 4 ```

Case 2:

 ``` nItems = 4 buf: 0 1 2 3 4 5 6 7 8 9 10 +---+---+---+---+---+---+---+---+---+---+---+ | x | | | | | | | | x | x | x | +---+---+---+---+---+---+---+---+---+---+---+ ^ ^ | | writePtr | | readPtr ((writePtr + 11) - readPtr) % 11 = ((1 + 11) - 8) % 11 = (12 - 8) % 11 = 4 % 11 = 4 ```

• BUT, we must make sure that the buffer does not fill up complete

Because otherwise, we cannot distinguish an empty buffer from a full buffer

The buffer is filled up when:

 ``` ( writePtr + 1 ) % N == readPtr ```

Example:

 ``` nItems = N-1 (full buffer !!!) buf: 0 1 2 3 4 ... N-1 +---+---+---+---+---+---+---+---+---+---+---+ | x | x | x | x | | x | x | x | x | x | x | +---+---+---+---+---+---+---+---+---+---+---+ ^ ^ | | | readPtr writePtr ```

• So, to prevent an ambiguous situation, we will not use the last empty slot

(Because if we fill the last empty slot, we will no be able to distinguish the filled state from the empty state)

• We can detect an empty buffer state using:

 ``` writePtr == readPtr ```

• We can detect the full buffer state using:

 ``` (writePtr+1)%N == readPtr ```

• Consider the following solution without using mutex locks:

 ``` Shared variables: int MAX; int nItems = 0; // Number of items in buffer int buf[MAX]; Writer: while ( true ) { int x; x = produce_next_value(); if ( (writePtr+1)%N == readPtr ) { // Buffer is full WAIT (BLOCK); // When BLOCKED, we need some // OTHER process to unblock it ! } buf[writePtr] = x; // Put x in buffer writePtr = (writePtr+1) % N; if ( reader is BLOCKED ) UNBLOCK reader; } Reader: while ( true ) { int x; if ( readPtr == writePtr ) { // Buffer is empty WAIT (BLOCK); // When BLOCKED, we need some // OTHER process to unblock it ! } x = buf[readPtr]; // Get x from buffer readPtr = (readPtr+1) % N; if ( writer is BLOCKED ) UNBLOCK writer; consume_value(x); } ```

• This solution can avoid the deadlock situation described above:

 ``` 1. Reader starts first 2. Reader finds buffer empty (readPtr == writePtr) 3. Reader blocks 4. Writer starts 5. Writer write an item to buffer 6. Writer detects a BLOCKED reader and UNBLOCKS the reader... ```

Problem solved... or so it seems :-)

• Sorry, but this solution can end in deadlock

But creating a blocking situation is very subtle....

• Here is a sequence of steps that will leads to deadlock:

 ``` 1. Reader starts first 2. Reader finds buffer empty (readPtr == writePtr) (Notice that the reader used writePtr from the writer !) 3. Writer starts !!! 4. Writer detects buffer not filled 5. Writer writes an item (The writePtr value read by the reader is now incorrect !!!) 4. Writer again detects buffer not filled 5. Writer again writes an item .... and so on until buffer is filled completely (All the while, the reader did not progress, so reader is NOT BLOCKED) k. Writer fills up the buffer and blocks k+1. NOW, the Reader finally progresses and BLOCKS ```

The system is now dead locked ---- because both reader and writer processes are blocked.

• NOTE:

Granted, the sequence of events is unlikely, but nevertheless it is possible...

• Lesson from the reader/writer problem

• There are different kinds of synchronization problems

• Different kinds of synchronization problems will require different kinds of synchronization primitives to resolve the sharing problem

• Comment:

 Computer Scientists sometimes dream up strange synchronization problems (like the dining philosopher problem But these problems are usually an outflow of a practical problem (i.e., they formulate the practical synchronization problem in an amuzing manner) Usually, they encounter practical "race conditions" and must solve the synchronization problem When existing syncrhonization primitives does not work (in solving the new problem), they then know that they have a "new kind" of problem on their hands....

• Semaphore synchronization primitive

• A semaphore is a shared memory synchronization method that is more general than locks

• A semaphore does not have ownership requirements

Any thread can perform operations on a semaphore

• The semaphore concept (invented by Dijkstra) was inspired by the train system

Here is a picture of a "real life" semaphore:

• A semaphore is used on a stretch of rail where that is only one set of tracks.

A semaphore is places at the entrance of stretch of single-tracked rail:

• When a train passes a semaphore, it disables the power of train on the other end

So there can only be one train traveling on the stretch of rail !!!

(Older semaphores will only cause the signal at the other end to set to the danger state and the conductor must manually stop the train....)

• Definition of a (counting) semaphore

• Conceptually a (counting) semaphore consists of:

 a (integer) count variable, and a queue (of threads)

• When the semaphore is defined, the count variable is set to a given value and the queue is empty

Example:

 ``` semaphore s(10); // s.count is set to 10 ```

• There are 2 operations defined on a semaphore s:

• P(s): (pass - Dutch: passeer)

 The operation is trying to pass the semaphore (check point) If the semaphore's value is greater than zero, then: P(s) is successful (i.e., the thread continues execution) The semaphore's value is decremented by one If the semaphore's value is ZERO, then: P(s) is unsuccesfull (i.e., the thread will BLOCK - put in the semaphore queue) (The semaphore's value remains ZERO)

• V(s): (leave - Dutch: verlaten)

 This operation is make the train leave the guarded section This operation will always be successful Effect: If the semaphore's value is greater than zero, then: increment count by one If the semaphore's value is ZERO, and no thread is BLOCKED in the queue, then: set semaphore value to ONE If the semaphore's value is ZERO, and some thread(s) is BLOCKED in the queue, then: UNBLOCK one of the threads

Furthermore: the P(s) and V(s) operations are atomic

That means all the effects of each operations are completed without any interruption (no intervening action of any kind).

• Similar to locks, a semaphore can be used to access shared variables

Example code used to access a shared variable:

 ``` int x; // shared variable semaphore s(1); // shared semaphore variable thread i: ..... P(s); x = x + 1; V(s); ```

Because the semaphore is initialed to one, the number of times that P(s) will succeed is one time only.

Therefore, only one thread can pass P(s) at any time...

Mutual exclusion is ensured !

• Semaphores used to solve the reader/writer problem

• Example

 ``` Shared variables: int MAX (some given unspecified constant); int nItems = 0; // Number of items in buffer int buf[MAX]; semaphore writerSem(MAX); // MAX writer credits: let writer in MAX times semaphore readerSem(0); // 0 reader credits: block reader from entry Writer: while ( true ) { int x; x = produce_next_value(); P(writerSem); // Check if there are "writer credits" buf[writePtr] = x; // Put x in buffer writePtr = (writePtr+1) % N; V(readerSem); // Give one "reader credit" } Reader: while ( true ) { int x; P(readerSem); // Check if there are "writer credits" x = buf[readPtr]; // Get x from buffer readPtr = (readPtr+1) % N; V(writerSem); // Give one "writer credit" consume_value(x); } ```

Why it will work correctly:

 The writer can succeed at most MAX times; then it will block So if P(writerSem) succeeds, the writer can surely find an empty slot to write into The reader can only succeed if the writer lets it succeed The writer "gives" credits to the reader by using: V(readerSem) In turn, the reader - after taking an item from the buffer - "gives" credits to the writer by using: V(writerSem)

• Example Program: (Demo above code)

How to run the program:

 Right click on link and save in a scratch directory To compile:   CC -mt Semaphore.C To run:          ./a.out