UNIT-III Process-Synchronization-Threads-and-Deadlocks
UNIT-III Process-Synchronization-Threads-and-Deadlocks
Deadlocks
This comprehensive guide explores the critical concepts of process synchronization, threads, and deadlocks in operating
systems. We'll examine how processes communicate, the challenges of concurrent programming, methods for preventing
race conditions, and techniques for managing deadlocks. Understanding these concepts is essential for developing robust,
efficient, and error-free operating systems and applications.
At its core, IPC enables processes to share information, coordinate activities, and manage shared resources effectively.
This communication is challenging because processes typically operate in isolated address spaces for security and stability
reasons. The operating system must provide special mechanisms to bridge these isolated environments while maintaining
system integrity.
IPC mechanisms vary in complexity and functionality, ranging from simple data transfer methods to sophisticated
synchronization techniques. Common IPC approaches include shared memory, where processes access common memory
regions; message passing, where processes exchange discrete messages; and synchronization primitives like semaphores
and mutexes that coordinate process timing and resource access.
Effective IPC design must address several critical challenges: preventing race conditions where the outcome depends on
timing of operations; ensuring mutual exclusion so only one process accesses shared resources at a time; avoiding
deadlocks where processes wait indefinitely for resources; and maintaining data coherence across process boundaries.
As we explore IPC mechanisms in detail, we'll examine how they address these challenges and enable the concurrent,
cooperative processing that underpins modern operating systems.
The Critical Section Problem
The critical section problem is one of the most fundamental challenges in concurrent programming. A critical section is a
segment of code where a process accesses shared resources, such as variables, data structures, or peripheral devices. The
central requirement is that when one process is executing in its critical section, no other process should be allowed to
execute in its critical section simultaneously.
For any solution to the critical section problem to be valid, it must satisfy three essential properties:
Mutual Exclusion: If a process is executing in its critical section, no other processes can be executing in their critical
sections simultaneously.
Progress: If no process is executing in its critical section and some processes wish to enter their critical section, only
those processes not executing in their remainder section can participate in deciding which will enter its critical section
next, and this selection cannot be postponed indefinitely.
Bounded Waiting: There exists a bound on the number of times 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.
The challenge in designing solutions to the critical section problem is finding mechanisms that satisfy all three properties
while minimizing performance overhead. Various approaches have been developed, ranging from simple software-based
algorithms like Peterson's solution to hardware-supported atomic instructions and higher-level abstractions like
semaphores and monitors.
Understanding the critical section problem is essential because it forms the theoretical foundation for synchronization
mechanisms used in operating systems and concurrent applications.
Race Conditions
Race conditions occur when multiple processes or threads access and manipulate shared data concurrently, and the final
outcome depends on the particular order in which the accesses take place. These conditions represent one of the most
challenging and insidious bugs in concurrent programming because they can lead to unpredictable program behavior that
may only manifest under specific timing circumstances.
Consider a simple example where two processes attempt to increment a shared counter variable:
// Process 1
counter = counter + 1;
// Process 2
counter = counter + 1;
While this code appears straightforward, a race condition can occur because each process must execute three steps: read
the counter value, increment it, and write it back. If Process 1 reads the counter (initially 5), then Process 2 reads the same
value before Process 1 completes its write, both processes will write back 6 rather than the expected final value of 7.
Preventing race conditions requires synchronization mechanisms that ensure orderly access to shared resources. These
mechanisms, such as locks, semaphores, and monitors, establish mutual exclusion, ensuring that only one process can
access the shared resource at any given time. By properly synchronizing access to shared data, developers can eliminate
race conditions and build reliable concurrent systems.
Mutual Exclusion
Mutual exclusion is a property of concurrency control that prevents simultaneous access to a shared resource. It is a
fundamental requirement for preventing race conditions and ensuring correct behavior in concurrent systems. When
multiple processes or threads need to access a shared resource, mutual exclusion ensures that only one process can access
the resource at any given time.
The concept of mutual exclusion can be illustrated through an analogy: imagine a single-stall bathroom that can only
accommodate one person at a time. When someone enters the bathroom, they lock the door (acquire the lock), use the
facility (access the shared resource), and then unlock the door when they leave (release the lock). This mechanism ensures
that only one person can use the bathroom at any given time.
In computing systems, mutual exclusion is typically implemented using various synchronization primitives:
Locks/Mutexes: Basic synchronization objects that can be acquired and released by processes.
Semaphores: Synchronization constructs that maintain a count and can be used for both mutual exclusion and
signaling.
Monitors: High-level synchronization constructs that encapsulate shared data and the procedures that operate on it.
Safety: Only one process can execute in the critical section at a time.
Liveness: If multiple processes are competing for entry to their critical sections, one must eventually succeed.
Fairness: Each process should have a fair chance of entering its critical section to avoid starvation.
While mutual exclusion is essential for correct concurrent operation, it introduces the possibility of deadlocks, priority
inversion, and performance bottlenecks. Therefore, designing effective mutual exclusion mechanisms requires careful
consideration of these potential issues.
Hardware Solutions for Mutual Exclusion
Hardware solutions for mutual exclusion leverage special machine instructions to implement atomic operations that
cannot be interrupted. These hardware-supported approaches provide the foundation for higher-level synchronization
mechanisms and are typically more efficient than purely software-based solutions.
Test-and-Set Instruction
The Test-and-Set instruction atomically tests a memory location and sets it to a new value. It returns the original value,
allowing a process to determine if it was the first to modify the location. This atomic operation is the basis for
implementing simple locks:
Compare-and-Swap Instruction
The Compare-and-Swap (CAS) instruction atomically compares the contents of a memory location to a given value and, if
they are equal, modifies the contents of that location to a new given value. CAS is more powerful than Test-and-Set and
forms the basis for many lock-free and wait-free algorithms:
Fetch-and-Add Instruction
The Fetch-and-Add instruction atomically increments a memory location and returns its original value. This instruction is
particularly useful for implementing counters and tickets in synchronization algorithms:
Hardware solutions provide significant performance advantages over software-only approaches because they execute as a
single, uninterruptible instruction at the hardware level. Modern multiprocessor systems often include additional
instructions and memory ordering guarantees to support efficient synchronization primitives.
Introduction to Semaphores
Semaphores are one of the most versatile synchronization mechanisms in operating systems. Introduced by Edsger
Dijkstra in 1965, a semaphore is an integer variable that, apart from initialization, can only be accessed through two
atomic operations: wait (also called P or down) and signal (also called V or up).
In practice, the busy waiting in the wait operation is replaced with a more efficient implementation that blocks the process
and places it in a waiting queue when the semaphore value is not positive, thereby conserving CPU cycles.
Binary Semaphores: These semaphores can only take the values 0 and 1. They are often used for mutual exclusion,
similar to mutex locks.
Counting Semaphores: These semaphores can take arbitrary non-negative values. They are used to control access to
resources that have multiple instances, such as a pool of database connections.
Semaphores are powerful because they can be used to solve various synchronization problems beyond simple mutual
exclusion:
Signaling: A process can use a semaphore to signal another process that an event has occurred.
Resource Counting: Semaphores can track the number of available resources and block processes when none are
available.
Barrier Synchronization: Multiple processes can use semaphores to wait until all have reached a certain point in
execution.
While semaphores are versatile, they require careful implementation to avoid issues like deadlocks, priority inversion, and
incorrect signaling sequences. Modern operating systems provide semaphore implementations that handle these concerns
and integrate with process scheduling.
Peterson's Solution
Peterson's solution is a software-based algorithm for mutual exclusion that allows two processes to share a single-use
resource without conflict. Developed by Gary L. Peterson in 1981, this solution is notable for being one of the few correct
software solutions to the critical section problem without requiring special hardware instructions.
flag: A boolean array where flag[i] indicates that process i wants to enter its critical section.
turn: An integer variable that indicates whose turn it is to enter the critical section.
The pseudocode for Peterson's solution for two processes (0 and 1) is as follows:
// Process i (where i is 0 or 1)
flag[i] = true; // Indicate interest in entering critical section
turn = 1-i; // Give priority to the other process
while (flag[1-i] && turn == 1-i) {
// Busy wait until the other process is not interested
// or it is this process's turn
}
// Critical section
// ...
flag[i] = false; // Indicate departure from critical section
Peterson's solution satisfies all three requirements for a valid critical section solution:
While Peterson's solution is elegant and instructive, it has limitations. It only works for two processes, though it can be
extended to handle more processes with increasing complexity. Furthermore, it relies on busy waiting, which wastes CPU
cycles, and it assumes sequential consistency in memory operations, which may not hold on modern multiprocessor
architectures without memory barriers.
Despite these limitations, Peterson's solution remains an important theoretical foundation for understanding mutual
exclusion algorithms and serves as a building block for more sophisticated synchronization mechanisms.
The Producer/Consumer Problem
The Producer/Consumer problem (also known as the Bounded Buffer problem) is a classic synchronization scenario that
illustrates the challenges of coordinating processes that share a fixed-size buffer. In this problem, producer processes
generate data and place it into a buffer, while consumer processes remove and process this data.
This problem demonstrates several synchronization challenges: mutual exclusion for buffer access, condition
synchronization for buffer status (full/empty), and the need to coordinate multiple processes with different roles.
// Shared data
buffer_size = N; // Fixed buffer size
buffer[N]; // Shared buffer
int in = 0; // Index for next producer insertion
int out = 0; // Index for next consumer removal
// Semaphores
semaphore mutex = 1; // For mutual exclusion to the buffer
semaphore empty = N; // Counts empty buffer slots
semaphore full = 0; // Counts filled buffer slots
// Producer process
void producer() {
while (true) {
item = produce_item(); // Generate an item
// Consumer process
void consumer() {
while (true) {
wait(full); // Wait if buffer is empty
wait(mutex); // Enter critical section
This solution uses three semaphores: 'mutex' for mutual exclusion when accessing the buffer, 'empty' to count empty slots,
and 'full' to count filled slots. The solution ensures that producers wait when the buffer is full, consumers wait when it's
empty, and buffer access is properly synchronized.
The Producer/Consumer pattern appears frequently in real systems, such as in print spoolers, web servers (handling
request queues), and multimedia applications (processing data streams). Understanding this pattern is essential for
designing efficient concurrent systems with producer-consumer relationships.
Event Counters
Event counters are synchronization primitives that provide a more structured approach to coordinating activities between
concurrent processes than basic semaphores. Introduced by Dennis and Van Horn, event counters are particularly useful
for scenarios where processes need to track the occurrence of specific events in a system.
An event counter is an integer variable that can only be manipulated through three operations:
Unlike semaphores, event counters cannot be decremented, which eliminates certain classes of errors where a semaphore
might be decremented twice or incremented twice. Event counters naturally model scenarios where processes need to
wait for a specific number of events to occur before proceeding.
For example, in a system where processes generate and consume data items in sequence, we can use event counters to
coordinate production and consumption:
// Shared variables
event_counter produced = 0; // Counts items produced
event_counter consumed = 0; // Counts items consumed
buffer_size = N; // Maximum buffer size
// Producer process
void producer() {
while (true) {
item = produce_item();
// Consumer process
void consumer() {
while (true) {
// Wait for an item to be available
await(produced, read(consumed) + 1);
consume_item(item);
}
}
Event counters provide a clear model for sequencing activities and can be particularly useful for implementing barriers,
pipelines, and other coordination patterns. They also allow processes to wait for specific conditions without busy waiting,
which improves system efficiency.
While not as commonly implemented in modern operating systems as semaphores or monitors, event counters represent
an important conceptual tool in the design of synchronization mechanisms and concurrent algorithms.
Monitors
Monitors are high-level synchronization constructs that encapsulate both the shared data and the procedures that operate
on that data. Introduced by C.A.R. Hoare and Per Brinch Hansen in the 1970s, monitors provide a structured approach to
solving synchronization problems, addressing many of the difficulties associated with lower-level primitives like
semaphores.
The key property of monitors is that they enforce mutual exclusion automatically: only one process can be executing a
monitor procedure at any time. This eliminates the need for explicit locking, reducing the potential for errors like
forgotten locks or incorrect lock ordering.
Condition variables within monitors enable processes to wait for specific conditions to be satisfied. They support two
primary operations:
wait(c): Suspends the calling process on condition variable c, releasing the monitor lock
signal(c): Resumes one process waiting on condition variable c (if any)
monitor ProducerConsumer {
int buffer[N];
int count = 0, in = 0, out = 0;
condition not_full, not_empty;
procedure insert(item) {
if (count == N)
wait(not_full); // Wait if buffer is full
buffer[in] = item;
in = (in + 1) % N;
count++;
item = buffer[out];
out = (out + 1) % N;
count--;
Monitors are implemented in several programming languages, including Java (synchronized methods), C# (lock
statement), and Ada (protected objects). They represent a significant advancement in concurrent programming by
providing a higher-level abstraction that reduces the complexity and potential for errors compared to lower-level
synchronization primitives.
Message Passing
Message passing is a communication mechanism that allows processes to send and receive messages, providing both data
transfer and synchronization. Unlike shared memory approaches, message passing doesn't require processes to share
memory space, making it suitable for distributed systems and microservices architectures.
Synchronization Modes
Synchronous: The sender blocks until the message is received, providing built-in synchronization.
Asynchronous: The sender continues execution after sending the message, which is buffered for later delivery.
Addressing Schemes
Isolation: Processes don't share memory, reducing the potential for interference.
Scalability: Works well in distributed environments where processes run on different machines.
Flexibility: Can be implemented with various semantics to meet different requirements.
Simplicity: The programming model can be simpler than shared memory with explicit locks.
// Producer process
void producer() {
while (true) {
item = produce_item();
send(consumer_id, item); // Send item to consumer
}
}
// Consumer process
void consumer() {
while (true) {
item = receive(); // Receive item from any producer
consume_item(item);
}
}
Modern operating systems and programming frameworks provide various message passing mechanisms, including pipes,
message queues, remote procedure calls (RPC), and sockets. In distributed systems, message-oriented middleware like
RabbitMQ, Apache Kafka, and gRPC facilitates sophisticated message passing patterns.
The Readers-Writers Problem
The Readers-Writers problem is a classic synchronization challenge that models scenarios where a shared resource (such
as a database or file) can be accessed by multiple processes simultaneously for reading, but must be accessed exclusively
for writing to prevent data inconsistency. This problem appears frequently in real-world systems like database
management, file systems, and content management platforms.
There are several variations of the Readers-Writers problem, with different priority policies:
First Readers-Writers Problem: Readers have priority. No reader should wait unless a writer has already obtained
permission to access the resource.
Second Readers-Writers Problem: Writers have priority. Once a writer is ready to write, no new readers should be
admitted.
Third Readers-Writers Problem: No starvation for either readers or writers. Both reader and writer processes have
fair access to the resource.
// Shared variables
semaphore mutex = 1; // For mutual exclusion to readcount
semaphore wrt = 1; // For exclusive writing
int readcount = 0; // Count of active readers
// Reader process
void reader() {
while (true) {
wait(mutex); // Enter critical section for readcount
readcount++;
if (readcount == 1) // First reader gets write lock
wait(wrt);
signal(mutex); // Exit critical section for readcount
// Writer process
void writer() {
while (true) {
wait(wrt); // Get exclusive access
This solution gives preference to readers, as new readers can join ongoing reading operations without waiting as long as at
least one reader is already active. However, this can lead to writer starvation if readers arrive continuously. The second and
third variations of the problem address this issue with different trade-offs between reader and writer priorities.
The Dining Philosophers Problem
The Dining Philosophers problem, formulated by Edsger Dijkstra in 1965, is a classic thought experiment that illustrates
synchronization challenges in concurrent systems, particularly deadlock and resource allocation. This problem has
become a fundamental teaching tool in operating systems courses because it elegantly captures the essence of concurrent
resource allocation problems.
The challenge is to design an algorithm that allows the philosophers to eat without causing deadlock or starvation. A naive
solution where each philosopher picks up the left fork and then the right fork can lead to deadlock if all philosophers
simultaneously pick up their left forks and then wait indefinitely for their right forks to become available.
Assign a unique number to each fork and require philosophers to pick up the lower-numbered fork first. This breaks the
circular wait condition necessary for deadlock.
void philosopher(int i) {
while (true) {
think();
pickup_fork(first_fork);
pickup_fork(second_fork);
eat();
putdown_fork(second_fork);
putdown_fork(first_fork);
}
}
Arbitrator Solution
Use a central authority (waiter) that allows only four philosophers to attempt to eat simultaneously, ensuring that at least
one philosopher can acquire both forks.
Chandry-Misra-Haas Solution
A distributed solution where forks can be "dirty" or "clean," and philosophers request forks with messages. This solution
works for any number of philosophers and avoids centralized control.
The Dining Philosophers problem demonstrates how subtle design choices in resource allocation can lead to deadlock,
starvation, or efficient concurrent execution. The solutions illustrate various approaches to deadlock prevention, including
resource ordering, limited allocation, and state-based protocols.
Critical Region Concept
The critical region is a high-level synchronization construct that provides structured access to shared variables in
concurrent programming. Introduced by Brinch Hansen and Hoare as an alternative to lower-level primitives like
semaphores, critical regions aim to make concurrent programs more readable, maintainable, and less prone to
synchronization errors.
Explicit Association: Critical regions are explicitly associated with the shared variables they protect, making the code's
intent clearer.
Mutual Exclusion: Only one process can execute in a critical region associated with a particular shared variable at any
time.
Condition Synchronization: The "when" clause allows a process to wait until a specific condition becomes true before
entering the critical region.
Simplicity: The construct clearly shows which shared variables are being protected.
Safety: The compiler can verify that shared variables are only accessed within appropriate critical regions.
Structured: Critical regions follow block structure, reducing the risk of forgetting to release locks.
shared buffer = {
items: array[N] of item,
count: 0,
in: 0,
out: 0
}
// Producer process
void producer() {
while (true) {
item = produce_item();
// Consumer process
void consumer() {
while (true) {
region buffer when count > 0 do
item = buffer.items[out];
out = (out + 1) % N;
count--;
end
consume_item(item);
}
}
While critical regions are not widely implemented in mainstream programming languages, their influence can be seen in
modern synchronization constructs like Java's synchronized blocks and C#'s lock statement. The concept of associating
synchronization with specific shared resources remains a valuable principle in concurrent programming.
Conditional Critical Regions
Conditional critical regions (CCRs) extend the basic critical region concept by adding a conditional entry feature. This
allows a process to specify a condition that must be satisfied before it can enter the critical section, enabling more fine-
grained synchronization and reducing the need for busy waiting.
The syntax for a conditional critical region typically follows this pattern:
When a process encounters a conditional critical region, the following sequence occurs:
shared buffer = {
items: array[N] of item,
count: 0,
in: 0,
out: 0
}
// Producer process
void producer() {
while (true) {
item = produce_item();
// Consumer process
void consumer() {
while (true) {
region buffer when (count > 0) do
item = buffer.items[out];
out = (out + 1) % N;
count--;
end region
consume_item(item);
}
}
Conditional critical regions provide a more elegant solution to many synchronization problems compared to using
separate semaphores or explicit signaling. They combine mutual exclusion and condition synchronization in a single
construct, making concurrent code more readable and less error-prone. However, the implementation of CCRs requires
sophisticated runtime support to efficiently manage the waiting processes and condition evaluation.
Monitors as Synchronization Constructs
Monitors represent one of the most sophisticated high-level synchronization constructs, combining data encapsulation
with synchronization mechanisms. As we explore monitors more deeply, it's essential to understand their internal
structure and behavior, particularly how they manage waiting processes and handle condition synchronization.
When multiple processes attempt to enter a monitor simultaneously, they are placed in an entry queue. The monitor
guarantees that only one process can be active inside the monitor at any time, enforcing mutual exclusion automatically.
Each condition variable within a monitor has an associated wait queue. When a process executes a wait operation on a
condition variable, it:
Signaling Mechanisms
Signal-and-continue: When a process signals a condition variable, it continues execution while the signaled process is
moved to the entry queue (not immediately activated).
Signal-and-wait: When a process signals a condition variable, it is suspended and placed in the entry queue, while the
signaled process is immediately activated.
Here's a more detailed example of a monitor implementation for the bounded buffer problem:
monitor BoundedBuffer {
const int N = 10; // Buffer size
item buffer[N]; // Shared buffer
int count = 0, in = 0, out = 0;
condition not_full, not_empty;
void deposit(item x) {
if (count == N)
wait(not_full); // Wait if buffer is full
buffer[in] = x;
in = (in + 1) % N;
count++;
item withdraw() {
if (count == 0)
wait(not_empty); // Wait if buffer is empty
item x = buffer[out];
out = (out + 1) % N;
count--;
While monitors require language or runtime support, their benefits for program structure and error reduction make them
valuable in complex concurrent systems. Languages like Java implement monitor-like constructs through the
synchronized keyword and wait/notify methods.
Concurrent Programming Languages
Concurrent programming languages are specifically designed or extended to support the development of programs with
multiple concurrent activities. These languages incorporate synchronization primitives and concurrency models directly
into their syntax and semantics, making concurrent programming more accessible and less error-prone compared to
using library-based approaches in sequential languages.
Process-Oriented Languages
These languages treat processes (or tasks) as the fundamental units of concurrency. Examples include:
Object-Oriented Concurrency
Modern concurrent languages are increasingly adopting higher-level abstractions that hide the complexities of low-level
thread management and synchronization. Models like actors, communicating sequential processes, and software
transactional memory provide structured approaches to concurrency that reduce the likelihood of errors like race
conditions and deadlocks.
The choice of concurrent programming language significantly impacts development productivity, program correctness,
and performance in concurrent systems. Understanding the concurrency models and primitives provided by different
languages is essential for effective concurrent programming.
Communicating Sequential Processes (CSP)
Communicating Sequential Processes (CSP) is a formal language for describing patterns of interaction in concurrent
systems. Developed by C.A.R. Hoare in 1978, CSP has had a profound influence on both theoretical computer science and
practical concurrent programming languages and libraries.
Processes: Independent entities that execute sequentially and interact with other processes
Channels: Named communication links between processes that support synchronous message passing
Events: Atomic interactions like sending or receiving a message
Choice: The ability for a process to offer multiple possible interactions and proceed with whichever one occurs first
CSP provides a mathematical foundation for reasoning about concurrent systems, allowing formal verification of
properties like deadlock freedom and liveness. This mathematical rigor distinguishes CSP from many other approaches to
concurrency.
In CSP, communication between processes is synchronous by default, meaning both the sender and receiver must be
ready to communicate for the exchange to occur. This two-way handshake eliminates many buffering and timing issues
that complicate asynchronous communication models.
Here, "³" indicates sequential composition, "!" denotes sending a message, "?" denotes receiving a message, and "||"
represents parallel composition of processes.
Deadlock avoidance: The structured communication patterns can reduce deadlock risks
CSP remains relevant today as systems become increasingly concurrent and distributed, providing both theoretical tools
for verification and practical patterns for implementation.
Introduction to Deadlocks
A deadlock is a situation in a concurrent system where two or more processes are unable to proceed because each is
waiting for resources held by others in the set. Deadlocks represent one of the most challenging problems in concurrent
programming and operating systems, as they can cause parts of a system to become permanently blocked.
To understand deadlocks, consider a simple scenario with two processes (P1 and P2) and two resources (R1 and R2). If P1
holds R1 and requests R2, while simultaneously P2 holds R2 and requests R1, both processes will wait indefinitely,
resulting in a deadlock.
For a deadlock to occur, four conditions must be present simultaneously (known as the Coffman conditions):
1. Mutual Exclusion: At least one resource must be held in a non-sharable mode, meaning only one process can use it at a
time.
2. Hold and Wait: Processes already holding resources may request new resources.
3. No Preemption: Resources cannot be forcibly taken away from a process; they must be released voluntarily.
4. Circular Wait: A circular chain of processes exists, where each process holds resources that the next process in the
chain is waiting for.
Understanding these conditions is crucial because deadlock prevention, avoidance, detection, and recovery strategies are
based on addressing one or more of these conditions. For instance, deadlock prevention might focus on eliminating the
possibility of circular wait, while deadlock avoidance might manage resource allocation to ensure the system never enters
an unsafe state.
Deadlocks are particularly problematic because they can be difficult to reproduce and debug, often appearing only under
specific timing conditions or workloads. Furthermore, the increasing complexity of modern software systems, with their
numerous interdependent components, makes deadlock management an ongoing challenge in system design and
implementation.
Necessary Conditions for Deadlock
Deadlocks in computing systems occur under specific circumstances that require all four of the Coffman conditions to be
present simultaneously. Understanding these conditions in detail is essential for developing strategies to prevent, avoid, or
recover from deadlocks.
1. Mutual Exclusion
Resources involved in a deadlock must be non-sharable, meaning they can be used by only one process at a time. If a
resource can be simultaneously used by multiple processes (like read-only files), it cannot contribute to a deadlock
situation. Examples of non-sharable resources include:
This condition occurs when processes already holding resources request additional resources that cannot be immediately
allocated. The key aspect is that processes do not release their currently held resources while waiting for requested
resources. This creates a situation where resources are held by waiting processes and are unavailable to others that might
need them.
3. No Preemption
Under this condition, resources cannot be forcibly taken away from a process. Resources are only released voluntarily by
the process holding them, typically when the process has completed its task with the resource. Operating systems
generally do not preempt resources because:
The process may be in the middle of a critical operation with the resource
Preemption might leave resources in an inconsistent state
The process may need to perform cleanup operations before releasing the resource
4. Circular Wait
This condition exists when there is a circular chain of two or more processes, each waiting for a resource held by the next
process in the chain. For example:
This forms a cycle where no process can proceed because each is waiting for a resource held by another process in the
cycle.
It's important to note that all four conditions must be present simultaneously for a deadlock to occur. If even one condition
is absent, deadlock cannot happen. This insight forms the basis for deadlock prevention strategies, which typically focus
on ensuring that at least one of these conditions cannot occur in the system.
Understanding these conditions also helps in identifying potential deadlock situations during system design and in
developing effective detection and recovery mechanisms when prevention is not practical.
Resource Allocation Graph
The Resource Allocation Graph (RAG) is a powerful tool for modeling and analyzing resource allocation in a system to
determine the potential for deadlocks. It provides a visual representation of the relationship between processes and
resources, making it easier to identify circular wait conditions that may lead to deadlocks.
The key insight of the Resource Allocation Graph is that a deadlock exists in the system if and only if the graph contains a
cycle that satisfies certain conditions:
For systems with only a single instance of each resource type, a cycle in the graph is both necessary and sufficient for a
deadlock to exist.
For systems with multiple instances of some resource types, a cycle is necessary but not sufficient for a deadlock.
Additional analysis is required to determine if a deadlock actually exists.
Let's examine how the Resource Allocation Graph can be used to analyze a system:
1. Constructing the Graph: Create nodes for each process and resource in the system. Draw appropriate request and
assignment edges based on the current state of resource allocation and pending requests.
2. Cycle Detection: Use graph traversal algorithms to identify cycles in the graph.
3. Deadlock Analysis: For single-instance resources, any cycle indicates a deadlock. For multiple-instance resources,
additional analysis is needed to determine if enough resources exist to satisfy at least one process in the cycle.
In practical systems, maintaining and analyzing a Resource Allocation Graph can be computationally expensive, especially
in large systems with many processes and resources. However, the conceptual framework it provides is invaluable for
understanding deadlock situations and designing strategies to handle them.
Deadlock Prevention Strategies
Deadlock prevention strategies focus on designing systems that structurally eliminate at least one of the four necessary
conditions for deadlock. These strategies provide absolute guarantees against deadlocks but often come with significant
restrictions or overhead.
This approach is often the most difficult to implement since many resources inherently require exclusive access:
Two main approaches exist to prevent processes from holding resources while waiting for others:
Total Allocation: Require processes to request all needed resources at once before execution begins. If all resources
are not available, the process doesn't start.
Resource Release: Require processes to release all currently held resources before requesting new ones.
3. Allowing Preemption
This strategy allows the operating system to forcibly take resources from processes:
When a process requests a resource that cannot be immediately allocated, all resources currently held by the process
are preempted (taken away)
The preempted resources are added to the list of resources for which the process is waiting
The process is restarted only when it can regain its old resources and acquire the new ones
Utilization Impact: Prevention strategies often lead to lower resource utilization as resources may remain idle even
when they could be used
Throughput Reduction: Processes may be delayed or blocked more frequently
Starvation Risk: Some strategies may lead to certain processes being repeatedly denied resources
Programming Complexity: Developers must follow strict protocols when requesting resources
Deadlock prevention is most appropriate in systems where safety is paramount and the cost of deadlocks is extremely
high, such as in critical infrastructure, medical devices, or financial transaction systems.
Introduction to Deadlock Avoidance
Deadlock avoidance represents a middle ground between the restrictive approach of deadlock prevention and the reactive
approach of deadlock detection and recovery. Rather than eliminating the possibility of deadlocks structurally, avoidance
algorithms dynamically examine resource allocation states to ensure the system never enters an unsafe state that could
lead to a deadlock.
The key concept in deadlock avoidance is the distinction between safe and unsafe states:
Safe State: A state where there exists at least one sequence of resource allocations that allows all processes to
complete, even if they request their maximum resources.
Unsafe State: A state where no such sequence exists, meaning the system might enter a deadlock in the future.
The primary advantage of deadlock avoidance is that it allows more concurrency than prevention strategies while still
providing guarantees against deadlocks. However, it has several requirements and limitations:
Processes must declare their maximum resource needs in advance, which may be difficult to determine precisely
The set of processes must be fixed and known in advance
Resources must be released eventually
The algorithm introduces runtime overhead for evaluating the safety of states
The most well-known deadlock avoidance algorithm is the Banker's Algorithm, developed by Edsger Dijkstra. This
algorithm maintains information about available resources, maximum claims, and current allocations, and uses this
information to determine whether a state is safe.
In practice, deadlock avoidance is less commonly used than prevention or detection approaches, primarily due to its
requirements for advance knowledge and the overhead of safety checks. However, it remains an important theoretical
approach and is valuable in specific controlled environments where its assumptions can be met.
The Banker's Algorithm
The Banker's Algorithm, developed by Edsger Dijkstra, is the most well-known deadlock avoidance algorithm. Named after
a banking system where loans are managed to ensure bank solvency, this algorithm determines whether granting a
resource request will lead to a safe state or potentially to a deadlock.
Available: A vector of length m (where m is the number of resource types) indicating the number of available
resources of each type
Max: An n×m matrix defining the maximum demand of each process for each resource type
Allocation: An n×m matrix defining the number of resources of each type currently allocated to each process
Need: An n×m matrix indicating the remaining resource needs of each process (calculated as Max - Allocation)
1. Safety Algorithm
This determines whether a system is in a safe state by trying to find a sequence in which all processes can complete:
2. Resource-Request Algorithm
P0 1, 0 3, 2 2, 2
P1 2, 0 4, 2 2, 2
P2 1, 1 5, 3 4, 2
Available resources: A = 2, B = 1
Running the safety algorithm would find the sequence [P1, P0, P2], confirming this state is safe.
The Banker's Algorithm provides a way to avoid deadlocks while still allowing dynamic resource allocation. However, it
has limitations:
Despite these limitations, the Banker's Algorithm remains an important theoretical tool and is used in certain controlled
environments where its assumptions can be satisfied.
Deadlock Detection
Deadlock detection is a strategy that allows deadlocks to occur but implements mechanisms to identify when they have
happened. This approach is particularly useful in systems where deadlock prevention or avoidance strategies are too
restrictive or impractical, and where occasional deadlocks can be tolerated if they are promptly detected and resolved.
For systems with only one instance of each resource type, deadlock detection is relatively straightforward using a wait-for
graph:
The algorithm for detecting cycles in a wait-for graph can use standard graph traversal techniques like depth-first search,
with a time complexity of O(n²) where n is the number of processes.
For systems with multiple instances of some resource types, deadlock detection is more complex and typically uses a
variation of the Banker's Algorithm. The key difference is that we don't need to know the maximum claims of processes,
only their current requests.
The detection algorithm is similar to the safety algorithm in the Banker's Algorithm:
1. Let Work = Available and Finish[i] = false for all i that have non-zero resource requests
2. Find an i such that:
a. Finish[i] = false
b. Request[i] f Work
If no such i exists, go to step 4
3. Work = Work + Allocation[i]
Finish[i] = true
Go to step 2
4. If Finish[i] = false for some i, then the system is in a deadlock state,
and the processes with Finish[i] = false are deadlocked
Several policies exist for determining when to invoke the deadlock detection algorithm:
On every resource request: Provides immediate detection but imposes significant overhead
Periodically: Runs the algorithm at fixed intervals, balancing detection time against overhead
When system performance degrades: Invokes detection when resource utilization or process progress metrics
indicate potential issues
The choice of policy depends on factors like the frequency of deadlocks, the cost of detection, and the impact of
undetected deadlocks on system performance and reliability.
Deadlock detection is often combined with recovery mechanisms to resolve deadlocks once they are detected, making it a
complete strategy for handling deadlocks in systems where prevention and avoidance are not feasible.
Deadlock Recovery Techniques
Once a deadlock has been detected, the system must take action to break the deadlock and allow the affected processes to
continue. Deadlock recovery techniques are methods used to resolve deadlocks after they have occurred. These techniques
vary in their impact on the system and the processes involved.
Process Termination
One approach to breaking a deadlock is to terminate one or more of the deadlocked processes. This releases all resources
held by the terminated processes, potentially allowing other processes to proceed. There are two main strategies for
process termination:
Terminate All Deadlocked Processes: This approach is simple but drastic, as it may abort processes that have been
computing for a long time.
Terminate Processes One by One: Processes are terminated incrementally until the deadlock cycle is broken. After
each termination, the deadlock detection algorithm is run again to see if the deadlock persists.
Resource Preemption
Another approach is to preempt resources from some processes and give them to others until the deadlock cycle is broken.
This approach involves several considerations:
Selecting a Victim: Choose a process from which to preempt resources, considering factors similar to those for process
termination
Rollback: Roll back the victim process to a safe state, often requiring checkpointing mechanisms
Starvation: Ensure that the same process is not always chosen as the victim, which could lead to starvation
The resource preemption approach is more complex but potentially less disruptive than process termination, as it allows
processes to continue after a rollback rather than being completely aborted.
Combined Approaches
Recovery-Related Issues
Data Integrity: Ensure that data remains consistent after process termination or resource preemption
Cascading Rollbacks: Prevent situations where rolling back one process requires rolling back others
User Notification: Provide appropriate feedback to users about terminated processes or operations
Recovery Overhead: Minimize the performance impact of recovery operations
Effective deadlock recovery requires not only detecting deadlocks promptly but also implementing recovery strategies that
minimize disruption to the system and its users while maintaining data integrity and system stability.
Introduction to Threads
Threads are the fundamental units of CPU utilization within a process. They represent a sequence of instructions that can
be scheduled and executed independently by the operating system. Unlike separate processes, threads within the same
process share the same memory space and resources, enabling more efficient communication and resource sharing.
The thread model provides a way to achieve parallelism within a single application, allowing multiple activities to progress
concurrently. This is particularly valuable in modern multi-core systems, where different threads can execute
simultaneously on different processor cores.
Thread Components
Impact of Blocking Only the process blocks Only the thread blocks
Responsiveness: Applications can remain responsive to user input while performing background tasks
Resource Sharing: Threads share the resources of the process, reducing overhead
Economy: Thread creation and context switching are less expensive than for processes
Scalability: Applications can take advantage of multiprocessor architectures
Understanding threads is essential for developing efficient, responsive applications in modern computing environments,
particularly as multi-core processors become ubiquitous.