Understanding Process Synchronization Techniques
Understanding Process Synchronization Techniques
way that no two processes can have access to the same shared data and [Link] is
specially needed in a multi-process system when multiple processes are running
together, and more than one processes try to gain access to the same shared resource
or data at the same time.
This can lead to the inconsistency of shared data. So the change made by one process
not necessarily reflected when other processes accessed the same shared data. To avoid
this type of inconsistency of data, the processes need to be synchronized with each
other.
For Example, process A changing the data in a memory location while another process
B is trying to read the data from the same memory location. There is a high probability
that data read by the second process will be erroneous.
Sections of a Program
Entry Section: It is part of the process which decides the entry of a particular
process.
Critical Section: This part allows one process to enter and modify the shared
variable.
Exit Section: Exit section allows the other process that are waiting in the Entry
Section, to enter into the Critical Sections. It also checks that a process that
finished its execution should be removed through this Section.
Remainder Section: All other parts of the Code, which is not in Critical, Entry,
and Exit Section, are known as the Remainder Section.
The entry to the critical section is handled by the wait() function, and it is
represented as P().
The exit from a critical section is controlled by the signal() function, represented
as V().
In the critical section, only a single process can be executed. Other processes, waiting
to execute their critical section, need to wait until the current process completes its
execution.
The solution to the critical section problem must satisfy the following conditions −
Mutual Exclusion
Mutual exclusion implies that only one process can be inside the critical section
at any time. If any other processes require the critical section, they must wait
until it is free.
Progress
Progress means that if a process is not using the critical section, then it should
not stop any other process from accessing it. In other words, any process can
enter a critical section if it is free.
Bounded Waiting
Bounded waiting means that each process must have a limited waiting time. Itt
should not wait endlessly to access the critical section.
Peterson’s Solution
do {
flag[i] = true;
turn = j;
while (flag[j] && turn == j);
/* critical section */
flag[i] = false;
/* remainder section */
}
while (true);
To prove 1, we note that each Pi enters its critical section only if either flag[j] == false
or turn == i. Also note that, if both processes can be executing in their critical sections
at the same time, then flag[0] == flag[1] == true. These two observations indicate that
P0 and P1 could not have successfully executed their while statements at about the
same time, since the value of turn can be either 0 or 1 but cannot be both. Hence, one
of the processes — say, Pj — must have successfully executed the while statement,
whereas Pi had to execute at least one additional statement (“turn == j”). However, at
that time, flag[j] == true and turn == j, and this condition will persist as long as Pj is
in its critical section; as a result, mutual exclusion is preserved.
To prove properties 2 and 3, we note that if a process is stuck in the while loop with
the condition flag[j] == true and turn == j, process Pi can be prevented from entering
the critical section only; this loop is the only one possible. flag[j] will be == false, and
Pi can enter its critical section if Pj is not ready to enter the critical section. If Pj has
set, flag[j] = true and is also executing in its while statement, then either turn == i or
turn == j. If turn == i, Pi will enter the critical section then. Pj will enter the critical
section, If turn == j. Although once Pj exits its critical section, it will reset flag[j] to
false, allowing Pi to enter its critical section. Pj must also set turn to i, if Pj resets
flag[j] to true. Hence, since Pi does not change the value of the variable turn while
executing the while statement, Pi will enter the critical section (progress) after at most
one entry by Pj (bounded waiting).
Peterson Solution
Peterson's solution is widely used solution to critical section problems. This algorithm
was developed by a computer scientist Peterson that's why it is named as a Peterson's
solution.
In this solution, when a process is executing in a critical state, then the other process
only executes the rest of the code, and the opposite can happen. This method also helps
to make sure that only a single process runs in the critical section at a specific time.
Semaphores
Semaphores(S) are integer variables that are used to solve the critical section problem
by using two atomic operations, wait() and signal(), apart from initialization that are
used for process synchronization.
The definitions of wait and signal are as follows −
wait(S)
{
while (S<=0);
//no-operation
S--;
}
signal(S)
{
S++;
}
Types of Semaphores
There are two main types of semaphores i.e. counting semaphores and binary
semaphores.
Counting Semaphores
These are integer value semaphores and have an unrestricted value domain.
These semaphores are used to coordinate the resource access, where the
semaphore count is the number of available resources. If the resources are added,
semaphore count automatically incremented and if the resources are removed,
the count is decremented.
Binary Semaphores
The binary semaphores are like counting semaphores but their value is restricted
to 0 and 1. The wait operation only works when the semaphore is 1 and the signal
operation succeeds when semaphore is 0.
Advantages of Semaphores
Some of the advantages of semaphores are as follows −
Semaphores allow only one process into the critical section. They follow the
mutual exclusion principle strictly and are much more efficient than some other
methods of synchronization.
There is no resource wastage because of busy waiting in semaphores as
processor time is not wasted unnecessarily to check if a condition is fulfilled to
allow a process to access the critical section.
Semaphores are implemented in the machine independent code of the
microkernel. So they are machine independent.
Disadvantages of Semaphores
Some of the disadvantages of semaphores are as follows −
Bounded buffer problem, which is also called producer consumer problem, is one of
the classic problems of synchronization.
A producer tries to insert data into an empty slot of the buffer. A consumer tries to
remove data from a filled slot in the [Link] two processes won't produce the
expected output if they are being executed concurrently.
There needs to be a way to make the producer and consumer work in an independent
manner.
One solution of this problem is to use semaphores.
m, a binary semaphore which is used to acquire and release the lock.
empty, a counting semaphore whose initial value is the number of slots in the
buffer, since, initially all slots are empty.
full, a counting semaphore whose initial value is 0.
At any instant, the current value of empty represents the number of empty slots in the
buffer and full represents the number of occupied slots in the buffer.
Producer operation
do
{
// wait until empty > 0 and then decrement 'empty'
//produce an item in nextp
wait(empty);
// acquire lock
wait(mutex);
// perform the insert operation in a slot I,e add nextp to buffer */
// release lock
signal(mutex);
// increment 'full'
signal(full);
}
while(TRUE)
Then it decrements the empty semaphore because, there will now be one less
empty slot, since the producer is going to insert data in one of those slots.
Then, it acquires lock on the buffer, so that the consumer cannot access the buffer
until producer completes its operation.
After performing the insert operation, the lock is released and the value of full is
incremented because the producer has just filled a slot in the buffer.
Readers-Writers Problem
The readers-writers problem relates to an object such as a file that is shared between
multiple processes. Some of these processes are readers i.e. they only want to read
the data from the object and some of the processes are writers i.e. they want to write
into the object.
The Problem Statement
There is a shared resource which should be accessed by multiple processes. There
are two types of processes in this context. They are reader and writer. Any number
of readers can read from the shared resource simultaneously, but only
one writer can write to the shared resource. When a writer is writing data to the
resource, no other process can access the resource. A writer cannot write to the
resource if there are non zero number of readers accessing the resource at that time
The Solution
From the above problem statement, it is evident that readers have higher priority
than writer. If a writer wants to write to the resource, it must wait until there are no
readers currently accessing that resource.
Here, we use one mutex m and a semaphore w. An integer variable read_count is used
to maintain the number of readers currently accessing the resource. The
variable read_count is initialized to 0. A value of 1 is given initially to m and w.
Instead of having the process to acquire lock on the shared resource, we use the
mutex m to make the process to acquire and release lock whenever it is updating
the read_count variable.
.
The code for the writer process looks like this:
while(TRUE)
{
wait(w);
signal(w);
}
And, the code for the reader process looks like this:
while(TRUE)
{
//acquire lock
wait(m);
read_count++;
if(read_count == 1)
wait(w);
//release lock
signal(m);
// acquire lock
wait(m);
read_count--;
if(read_count == 0)
signal(w);
// release lock
signal(m);
}
As seen above in the code for the writer, the writer just waits on the w semaphore
until it gets a chance to write to the resource.
After performing the write operation, it increments w so that the next writer can
access the resource.
On the other hand, in the code for the reader, the lock is acquired whenever
the read_count is updated by a process.
When a reader wants to access the resource, first it increments
the read_count value, then accesses the resource and then decrements
the read_count value.
The semaphore w is used by the first reader which enters the critical section and
the last reader which exits the critical section.
The reason for this is, when the first readers enters the critical section, the writer
is blocked from the resource. Only new readers can access the resource now.
Similarly, when the last reader exits the critical section, it signals the writer using
the w semaphore because there are zero readers now and a writer can have the
chance to access the resource.
From the problem statement, it is clear that a philosopher can think for an indefinite
amount of time. But when a philosopher starts eating, he has to stop at some point of
time. The philosopher is in an endless cycle of thinking and eating.
An array of five semaphores, stick[5], for each of the five chopsticks.
while(TRUE)
{
wait(stick[i]);
i=5
(5+1)%5=1
i=2
(2+1)%5=3
/* mod is used because if i=5, next chopstick is 1 (dining table is circular) */
wait(stick[(i+1) % 5]);
/* eat */
signal(stick[i]);
signal(stick[(i+1) % 5]);
/* think */
}
When a philosopher wants to eat the rice, he will wait for the chopstick at his left and
picks up that chopstick. Then he waits for the right chopstick to be available, and then
picks it too. After eating, he puts both the chopsticks down.
But if all five philosophers are hungry simultaneously, and each of them pickup one
chopstick, then a deadlock situation occurs because they will be waiting for another
chopstick forever. The possible solutions for this are:
A philosopher must be allowed to pick up the chopsticks only if both the left and
right chopsticks are available.
Allow only four philosophers to sit at the table. That way, if all the four
philosophers pick up four chopsticks, there will be one chopstick left on the
table. So, one philosopher can start eating and eventually, two chopsticks will be
available. In this way, deadlocks can be avoided.
Monitors
Monitors and semaphores are used for process synchronization and allow processes to
access the shared resources using mutual exclusion. Monitors are a synchronization
construct that were created to overcome the problems caused by semaphores
Monitors are abstract data types and contain shared data variables and procedures. The
shared data variables cannot be directly accessed by a process and procedures are
required to allow a single process to access the shared data variables at a time.
This is demonstrated as follows:
monitor monitorName
{
data variables;
Procedure P1(....)
{
}
Procedure P2(....)
{
}
Procedure Pn(....)
{
}
Initialization Code(....)
{
}
}
Only one process can be active in a monitor at a time. Other processes that need to
access the shared variables in a monitor have to line up in a queue and are only provided
access when the previous process release the shared variables.
A monitor has four components: initialization, private data, monitor procedures, and
monitor entry queue.
The initialization component contains the code that is used exactly once when the
monitor is created,
The private data section contains all private data, including private procedures,that can
only be used within the monitor. Thus, these private items are not visible from outside
of the monitor. The monitor procedures are procedures that can be called from outside
of the monitor. The monitor entry queue contains all threads that called monitor
procedures but have not been granted permissions.
Signal & Wait-P either waits until Q leaves the monitor or waits for the conditions
Signal & Continue- Q either waits until P leaves the monitor or waits for the conditions
CPU Scheduling is a process of determining which process will own CPU for
execution while another process is on hold.
P1,P2,P3
The main task of CPU scheduling is to make sure that whenever the CPU remains idle,
the OS at least select one of the processes available in the ready queue for execution.
The selection process will be carried out by the CPU scheduler. It selects one of the
processes in memory that are ready for execution.
Preemptive Scheduling
In Preemptive Scheduling, the tasks are assigned with their priorities. Sometimes it is
important to run a task with a higher priority before another lower priority task, even
if the lower priority task is still running. The lower priority task holds for some time
and resumes when the higher priority task finishes its execution.
P1-priority(2)
P2-Priority(3)
P3-Priority(1)
P1 & P2 are in ready state
P1 Goes to run state-----
P3 enters in to ready state
P2,P3 are in ready state.
Since P3 has highest priority thanP1 and P2 though p1 has not completed its task, it
has to be removed and OS allocates P3.
P3 completes it’s job
P1 enters the run state then P2.
Non-Preemptive Scheduling
In this type of scheduling method, the CPU has been allocated to a specific process.
The process that keeps the CPU busy will release the CPU either by switching context
or terminating. It is the only method that can be used for various hardware platforms.
P1 enters first (5msec)—run state
P2 enters(50msec)
P3 enters(3msec)
P2-25mesc—request for I/O---
Context Switching involves storing the context or state of a process so that it can be
reloaded when required and execution can be resumed from the same point as earlier.
This is a feature of a multitasking operating system and allows a single CPU to be
shared by multiple processes.
Let Process 1 is running. Process 1 is switched out and Process 2 is switched in because
of an interrupt or a system call. Context switching involves saving the state of Process
1 into PCB1 and loading the state of process 2 from PCB2. After some time again a
context switch occurs and Process 2 is switched out and Process 1 is switched in again.
This involves saving the state of Process 2 into PCB2 and loading the state of process
1 from PCB1.
[Link] –A measure of the work done by CPU is the number of processes being
executed and completed per unit time. This is called throughput. The throughput may
vary depending upon the length or duration of processes.
[Link] around time –For a particular process, an important criteria is how long it takes
to execute that process. The time elapsed from the time of submission of a process to
the time of completion is known as turnaround time.
Turnaround time is the sum of times spent waiting to get into memory, waiting in ready
queue, executing in CPU and waiting for I/O.
[Link] time –A scheduling algorithm does not affect the time required to complete
the process once it starts execution. It only affects the waiting time of a process i.e.
time spent by a process waiting in the ready queue.
[Link] time –The time taken from submission of process of request until the first
response is produced. This measure is called response time.
First Come First Serve (FCFS): Simplest scheduling algorithm that schedules
according to arrival times of processes. First come first serve scheduling algorithm
states that the process that requests the CPU first is allocated the CPU first. It is
implemented by using the FIFO queue. FCFS is a non-preemptive scheduling
algorithm.
First come first serve suffers from convoy effect.
Convoy Effect is a situation where many processes need to use a resource for short
time are blocked by one process holding that resource for a long time. This essentially
leads to poor utilization of resources and hence poor performance.
Shortest Job First (SJF) Scheduling in OS
Shortest Job First is a Preemptive or Non-Preemptive algorithm. In the shortest job first
algorithm, the job having shortest or less burst time will get the CPU first. It is the best
approach to minimize the waiting time. It is simple to implement in the batch operating
system because in this CPU time is known in advance.
Characteristics of Shortest Job First Scheduling
SJF algorithm is helpful in batch operating where the waiting time for job
completion is not critical.
P1-8msec
P2-3msec
P3-5msec
SJF improves the throughput of the process by ensuring that the shorter jobs are
executed first, thus the possibility of less turnaround time.
SJF enhances the output of the job by executing the process, which is having the
shortest burst time.
Deadlock is a situation that occurs in OS when any process enters a waiting state
because another waiting process is holding the demanded resource. Deadlock is a
common problem in multi-processing where several processes share a specific type of
mutually exclusive resource known as a soft lock or software.
Example of Deadlock
Example of deadlock
Deadlock is a situation where a set of processes are blocked because each process is
holding a resource and waiting for another resource acquired by some other process.
For example, in the below diagram, Process 1 is holding Resource 1 and waiting for
resource 2 which is acquired by process 2, and process 2 is waiting for resource 1.
Deadlocks can be avoided by avoiding at least one of the four conditions, because all
this four conditions are required simultaneously to cause deadlock.
A process in operating systems uses different resources and uses resources in the
following way.
1) Requests a resource – If the request is not granted immediately, then the requesting
process must wait until it can acquire the resource
2) Use the resource – operate on the resource.
3) Releases the resource –release the resource once the operation is completed.
Deadlock Characterization
A deadlock situation occurs if the following four conditions hold simultaneously
Mutual Exclusion
There should be a resource that can only be held by one process at a time. In the
diagram below, there is a single instance of Resource 1 and it is held by Process
1 only.
No Preemption
A resource cannot be preempted from a process by force. A process can only
release a resource voluntarily. In the diagram below, Process 2 cannot preempt
Resource 1 from Process 1. It will only be released when Process 1 relinquishes
it voluntarily after its execution is complete.
Circular Wait
A process is waiting for the resource held by the second process, which is
waiting for the resource held by the third process and so on, till the last process
is waiting for a resource held by the first process. This forms a circular chain.
For example: Process 1 is allocated Resource2 and it is requesting Resource 1.
Similarly, Process 2 is allocated Resource 1 and it is requesting Resource 2. This
forms a circular wait loop.
Resource allocation graph consists of all the information which is related to all the
instances of the resources means the information about available resources and the
resources which the process is being using. In the Resource Allocation Graph, we use
a circle to represent the process and rectangle to represent the resource.
Process Vertices
Resource Vertices
Single instance resource type: – In single instance resource type, we use only a single
dot inside the box. The single dot indicates that there is one instance of the resource.
Multiple instance resource type: – In multiple instance resource type, we use multiple
dots inside the box. Multiple dots indicate that there are various instances of the
resources.
2. Edges: – There are two types of edges we use in the resource allocation graph:
Assign Edges
Request Edges
Assign Edges: – We use an assign edge to represent the allocation of resources to the
process. We draw assign edges with the help of arrow in which the arrow head points
the process, and the process tail points the instance of the resource.
Request Edges: – We use request edge to signify the waiting state of the process. Just
like in assign edge, an arrow is used to draw arrow edge. Here, the arrow head points
the instance of a resource, and tail of the process points to the process. For example, if
a process needs ‘n’ instances of resource type, then we will draw ‘n’ assign edges.
For example:
Resource allocation graph
R1 R2 R1 R2
Process
1 0 0 1
P1
Process
0 1 1 0
P2
R1 R2 R2
R1
Process
1 0 0 1
P1
Process
0 1 1 0
P2
Process
0 1 0 0
P3
Availability = [0 0] + [0 1]
= [ 0 1]
Now, based on the current availability, we can fulfill the requirement of the P1 process.
So, we assign the requested resource to the process P1. When the process P1 executes
successfully, then we terminate the process P1.
Now we again calculate availability
Availablity = [0 1] [1 0]
= [1 1]
Now, again based on the current availability we fulfill the requirement of process P2
because the P2 process satisfies the requirement. So, we allocate the requested resource
to the Process P2. When the process P2 executes successfully, then it terminates, and
we again calculate the availability.
Availability = [1 1] + [0 1]
= [1 2]
So, in this example, there is a safe sequence P3, P1, P2, and all the processes are
executed successfully. So, we can say that system is in a safe state, and there is no
deadlock in the system.
←
Methods for Handling Deadlocks
1. Deadlock Prevention
Mutual Exclusion
To prevent this condition processes must be prevented from holding one or more
resources while simultaneously waiting for one or more others. There are several
possibilities for this:
o Require that all processes request all resources at one time. This can be
wasteful of system resources if a process needs one resource early in its
execution and doesn't need some other resource until much later.
o Require that processes holding resources must release them before
requesting new resources, and then re-acquire the released resources along
with the new ones in a single new request. This can be a problem if a
process has partially completed an operation using a resource and then
fails to get it re-allocated after releasing it.
o Either of the methods described above can lead to starvation if a process
requires one or more popular resources.
No Preemption
Circular Wait
One way to avoid circular wait is to number all resources, and to require that
processes request resources only in strictly increasing ( or decreasing ) order.
In other words, in order to request resource Rj, a process must first release all Ri
such that i >= j.
One big challenge in this scheme is determining the relative ordering of the
different resources
[Link] Avoidance
The general idea behind deadlock avoidance is to prevent deadlocks from ever
happening, by preventing at least one of the aforementioned conditions.
This requires more information about each process, and tends to lead to low
device utilization. ( I.e. it is a conservative approach. )
In some algorithms the scheduler only needs to know the maximum number of
each resource that a process might potentially use. In more complex algorithms
the scheduler can also take advantage of the schedule of exactly what resources
may be needed in what order.
When a scheduler sees that starting a process or granting resource requests may
lead to future deadlocks, then that process is just not started or the request is not
granted.
A resource allocation state is defined by the number of available and allocated
resources, and the maximum requirements of all processes in the system.
Safe State
A state is safe if the system can allocate all resources requested by all processes
( up to their stated maximums ) without entering a deadlock state.
More formally, a state is safe if there exists a safe sequence of processes { P0,
P1, P2, ..., PN } such that all of the resource requests for Pi can be granted using
the resources currently allocated to Pi and all processes Pj where j < i. ( I.e. if all
the processes prior to Pi finish and free up their resources, then Pi will be able
to finish also, using the resources that they have freed up. )
If a safe sequence does not exist, then the system is in an unsafe state,
which MAY lead to deadlock. ( All safe states are deadlock free, but not all
unsafe states lead to deadlocks. )
What happens to the above table if process P2 requests and is granted one more
tape drive?
Key to the safe state approach is that when a request is made for resources, the
request is granted only if the resulting allocation state is a safe one.
The resulting resource-allocation graph would have a cycle in it, and so the
request cannot be granted.
For resource categories that contain more than one instance the resource-
allocation graph method does not work, and more complex ( and less efficient )
methods must be chosen.
The Banker's Algorithm gets its name because it is a method that bankers could
use to assure that when they lend out resources they will still be able to satisfy
all their clients. ( A banker won't loan out a little money to start building a house
unless they are assured that they will later be able to loan out the rest of the
money to finish the house. )
When a process starts up, it must state in advance the maximum allocation of
resources it may request, up to the amount available on the system.
When a request is made, the scheduler determines whether granting the request
would leave the system in a safe state. If not, then the process must wait until
the request can be granted safely.
The banker's algorithm relies on several key data structures: (where n is the
number of processes and m is the number of resource categories. )
This process has not finished, but could with the given available
working set. If no such i exists, go to step 4.
Now that we have a tool for determining if a particular state is safe or not, we
are now ready to look at the Banker's algorithm itself.
This algorithm determines if a new request is safe, and grants it only if it is safe
to do so.
When a request is made ( that does not exceed currently available resources ),
pretend it has been granted, and then see if the resulting state is a safe one. If so,
grant the request, and if not, deny the request, as follows:
1. Let Request[ n ][ m ] indicate the number of resources of each type
currently requested by processes. If Request[ i ] > Need[ i ] for any process
i, raise an error condition.
2. If Request[ i ] > Available for any process i, then that process must wait
for resources to become available. Otherwise the process can continue to
step 3.
3. Check to see if the request can be granted safely, by pretending it has been
granted and then seeing if the resulting state is safe. If so, grant the request,
and if not, then the process must wait until its request can be granted
safely. The procedure for granting a request ( or pretending to for testing
purposes ) is:
Available = Available - Request
Allocation = Allocation + Request
Need = Need - Request
If deadlocks are not avoided, then another approach is to detect when they have
occurred and recover somehow.
In addition to the performance hit of constantly checking for deadlocks, a policy
/ algorithm must be in place for recovering from deadlocks, and there is potential
for lost work when processes must be aborted or have their resources preempted.
If each resource category has a single instance, then we can use a variation of
the resource-allocation graph known as a wait-for graph.
A wait-for graph can be constructed from a resource-allocation graph by
eliminating the resources and collapsing the associated edges, as shown in the
figure below.
An arc from Pi to Pj in a wait-for graph indicates that process Pi is waiting for a
resource that process Pj is currently holding.
Figure 7.9 - (a) Resource allocation graph. (b) Corresponding wait-for graph
The detection algorithm outlined here is essentially the same as the Banker's
algorithm, with two subtle differences:
o In step 1, the Banker's Algorithm sets Finish[ i ] to false for all i. The
algorithm presented here sets Finish[ i ] to false only if Allocation[ i ] is
not zero. If the currently allocated resources for this process are zero, the
algorithm sets Finish[ i ] to true. This is essentially assuming that IF all of
the other processes can finish, then this process can finish also.
Furthermore, this algorithm is specifically looking for which processes
are involved in a deadlock situation, and a process that does not have any
resources allocated cannot be involved in a deadlock, and so can be
removed from any further consideration.
o Steps 2 and 3 are unchanged
o In step 4, the basic Banker's Algorithm says that if Finish[ i ] == true for
all i, that there is no deadlock. This algorithm is more specific, by stating
that if Finish[ i ] == false for any process Pi, then that process is
specifically involved in the deadlock which has been detected.
Now suppose that process P2 makes a request for an additional instance of type
C, yielding the state shown below. Is the system now deadlocked?
When preempting resources to relieve deadlock, there are three important issues
to be addressed:
1. Selecting a victim - Deciding which resources to preempt from which
processes involves many of the same decision criteria outlined above.
2. Rollback - Ideally one would like to roll back a preempted process to a
safe state prior to the point at which that resource was originally allocated
to the process. Unfortunately it can be difficult or impossible to determine
what such a safe state is, and so the only safe rollback is to roll back all
the way back to the beginning.
3. Starvation - How do you guarantee that a process won't starve because
its resources are constantly being preempted? One option would be to use
a priority system, and increase the priority of a process every time its
resources get preempted.