Classical Problems of
Synchronization with Semaphore
Solution
With the evolution of multi-processors, tasks suffer due to overheads and traffic. To
manage the resources efficiently, synchronization is needed. Synchronization ensures
perfect execution of processes and controlled access to shared resources.
Synchronization in operating systems is often explained with the help of real-life
examples.
In this article, we will see a number of classical problems of synchronization as
examples of a large class of concurrency-control problems. In our solutions to the
problems, we use semaphores for synchronization, since that is the traditional way to
present such solutions. However, actual implementations of these solutions could use
mutex locks instead of binary semaphores.
Synchronization Problems with Semaphore
Solution
These problems are used for testing nearly every newly proposed synchronization
scheme. The following problems of synchronization are considered as classical
problems:
1. Bounded-buffer (or Producer-Consumer) Problem,
2. Dining-Philosophers Problem,
3. Readers and Writers Problem,
4. Sleeping Barber Problem
1) Bounded-Buffer (or Producer-Consumer) Problem
The Bounded Buffer problem is also called the producer-consumer problem. This
problem is generalized in terms of the Producer-Consumer problem. The solution to
this problem is, to create two counting semaphores “full” and “empty” to keep track
of the current number of full and empty buffers respectively. Producers produce a
product and consumers consume the product, but both use of one of the containers
each time.
In our problem, the producer and consumer processes share the following
data structures:
int n;
semaphore mutex = 1;
semaphore empty = n;
semaphore full = 0
We assume that the pool consists of n buffers, each capable of holding one item.
The mutex semaphore provides mutual exclusion for accesses to the buffer pool
and is initialized to the value 1. The empty and full semaphores count the
number of empty and full buffers. The semaphore empty is initialized to the
value n; the semaphore full is initialized to the value 0.
The code for the producer process is shown in Figure 5.9, and the code
for the consumer process is shown in Figure 5.10. Note the symmetry between
the producer and the consumer. We can interpret this code as the producer
producing full buffers for the consumer or as the consumer producing empty
buffers for the producer.
do {
wait(full);
wait(mutex);
...
/* remove an item from buffer to next consumed */
...
signal(mutex);
signal(empty);
...
/* consume the item in next consumed */
...
} while (true);
Figure : The structure of the consumer process.
2) Dining-Philosophers Problem
The Dining Philosopher Problem states that K philosophers seated around a circular
table with one chopstick between each pair of philosophers. There is one chopstick
between each philosopher. A philosopher may eat if he can pickup the two chopsticks
adjacent to him. One chopstick may be picked up by any one of its adjacent followers
but not both. This problem involves the allocation of limited resources to a group of
processes in a deadlock-free and starvation-free manner.
Consider five philosophers who spend their lives thinking and eating. The
philosophers share a circular table surrounded by five chairs, each belonging
to one philosopher. In the center of the table is a bowl of rice, and the table is laid
with five single chopsticks (Figure 5.13). When a philosopher thinks, she does
not interact with her colleagues. From time to time, a philosopher gets hungry
and tries to pick up the two chopsticks that are closest to her (the chopsticks
that are between her and her left and right neighbors). A philosopher may pick
up only one chopstick at a time. Obviously, she cannot pick up a chopstick that
is already in the hand of a neighbor. When a hungry philosopher has both her
chopsticks at the same time, she eats without releasing the chopsticks. When
she is finished eating, she puts down both chopsticks and starts thinking again.
One simple solution is to represent each chopstick with a semaphore. A philosopher
tries to grab a chopstick by executing a wait() operation on that semaphore. She
releases her chopsticks by executing the signal() operation on the appropriate
semaphores. Thus, the shared data are
semaphore chopstick[5];
do {
wait(chopstick[i]);
wait(chopstick[(i+1) % 5]);
...
/* eat for awhile */
...
signal(chopstick[i]);
signal(chopstick[(i+1) % 5]);
...
/* think for awhile */
...
} while (true);
Figure:The structure of philosopher i.
where all the elements of chopstick are initialized to 1. The structure of
philosopher i is shown in Figure.
Although this solution guarantees that no two neighbors are eating
simultaneously, it nevertheless must be rejected because it could create a
deadlock. Suppose that all five philosophers become hungry at the same time
and each grabs her left chopstick. All the elements of chopstick will now be
equal to 0. When each philosopher tries to grab her right chopstick, she will be
delayed forever.
Several possible remedies to the deadlock problem are replaced by:
• Allow at most four philosophers to be sitting simultaneously at the table.
• 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).
• Use an asymmetric solution—that is, an odd-numbered philosopher picks
up first her left chopstick and then her right chopstick, whereas an even
numbered philosopher picks up her right chopstick and then her left
chopstick.
3) Readers-Writers Problem
Suppose that a database is to be shared among several concurrent processes. Some of
these processes may want only to read the database, whereas others may want to
update (that is, to read and write) the database. We distinguish between these two
types of processes by referring to the former as readers and to the latter as writers.
Precisely in OS we call this situation as the readers-writers problem. Problem
parameters:
One set of data is shared among a number of processes.
Once a writer is ready, it performs its write. Only one writer may write at a time.
If a process is writing, no other process can read it.
If at least one reader is reading, no other process can write.
Readers may not write and only read.
In the solution to the first readers–writers problem, the reader processes
share the following data structures:
semaphore rw mutex = 1;
semaphore mutex = 1;
int read count = 0;
The semaphores mutex and rw mutex are initialized to 1; read count is
initialized to 0. The semaphore rw mutex is common to both reader and writer
do {
wait(rw mutex);
...
/* writing is performed */
...
signal(rw mutex);
} while (true);
Figure : The structure of a writer process
processes. The mutex semaphore is used to ensure mutual exclusion when the
variable read count is updated. The read count variable keeps track of how
many processes are currently reading the object. The semaphore rw mutex
functions as a mutual exclusion semaphore for the writers. It is also used by
the first or last reader that enters or exits the critical section. It is not used by
readers who enter or exit while other readers are in their critical sections.
The code for a writer process is shown in Figure 5.11; the code for a
reader process is shown in Figure 5.12. Note that, if a writer is in the critical
section and n readers are waiting, then one reader is queued on rw mutex, and
n − 1 readers are queued on mutex. Also observe that, when a writer executes
signal(rw mutex), 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
provide reader–writer locks on some systems. Acquiring a reader–writer lock
requires specifying the mode of the lock: either read or write access. When a
process wishes only to read shared data, it requests the reader–writer lock
in read mode. A process wishing to modify the shared data must request the
lock in write mode. Multiple processes are permitted to concurrently acquire
a reader–writer lock in read mode, but only one process may acquire the lock
for writing, as exclusive access is required for writers.
Reader–writer locks are most useful in the following situations:
do {
wait(mutex);
read count++;
if (read count == 1)
wait(rw mutex);
signal(mutex);
...
/* reading is performed */
...
wait(mutex);
read count--;
if (read count == 0)
signal(rw mutex);
signal(mutex);
} while (true);
Figure: The structure of a reader process.
Mutex Locks
A mutex lock (short for mutual exclusion lock) is a synchronization primitive used to
solve the critical-section problem by ensuring that only one process or thread can
access a critical section at a time.
Working:
Before entering the critical section, a process acquires the mutex lock.
After finishing the critical section, it releases the lock.
If a process tries to acquire a lock that is already held, it must wait until
the lock becomes available.
Implementation:
A simple implementation uses a boolean variable available:
acquire() {
while (!available)
; // busy wait
available = false;
}
release() {
available = true;
}
Spinlock:
This type of mutex lock involves busy waiting (called a spinlock), where the process
keeps checking the lock in a loop.
Advantages:
Simple to implement.
Efficient for short critical sections.
Disadvantages:
Wastes CPU cycles during busy wait.
Not ideal for single-processor systems.
Use Cases:
Used in operating systems, multithreaded applications, and Pthreads for
managing concurrent access to shared resources.