Deadlock
Deadlock
CHAPTER OBJECTIVES
• Illustrate how deadlock can occur when mutex locks are used.
• Define the four necessary conditions that characterize deadlock.
• Identify a deadlock situation in a resource allocation graph.
• Evaluate the four different approaches for preventing deadlocks.
• Apply the banker’s algorithm for deadlock avoidance.
• Apply the deadlock detection algorithm.
• Evaluate approaches for recovering from deadlock.
317
8.1 System Model
1. Request. The thread requests the resource. If the request cannot be granted
immediately (for example, if a mutex lock is currently held by another thread), then
the requesting thread must wait until it can acquire the resource.
2. Use. The thread can operate on the resource (for example, if the resource is a mutex
lock, the thread can access its critical section).
3. Release. The thread releases the resource.
The request and release of resources may be system calls, as explained in Chapter 2.
Examples are the request() and release() of a device, open() and close() of a file, and
allocate() and free() memory system calls. Similarly, as we saw in Chapter 6, request
and release can be accomplished through the wait() and signal() operations on
semaphores and through acquire() and release() of a mutex lock. For each use of a
kernel-managed resource by a thread, the operating system checks to make sure that the
thread has requested and has been allocated the resource. A system table records whether
each resource is free or allocated. For each resource that is allocated,
8.2 Deadlock in Multithreaded Applications
the table also records the thread to which it is allocated. If a thread requests a resource
that is currently allocated to another thread, it can be added to a queue of threads waiting
for this resource.
Aset of threads is in a deadlocked state when every thread in the set is waiting for an
event that can be caused only by another thread in the set. The events with which we are
mainly concerned here are resource acquisition and release. The resources are typically
logical (for example, mutex locks, semaphores, and files); however, other types of events
may result in deadlocks, including reading from a network interface or the IPC
(interprocess communication) facilities discussed in Chapter 3.
To illustrate a deadlocked state, we refer back to the dining-philosophers problem
from Section 7.1.3. In this situation, resources are represented by chopsticks. If all the
philosophers get hungry at the same time, and each philosopher grabs the chopstick on
her left, there are no longer any available chopsticks. Each philosopher is then blocked
waiting for her right chopstick to become available.
Developers of multithreaded applications must remain aware of the possibility of
deadlocks. The locking tools presented in Chapter 6 are designed to avoid race
conditions. However, in using these tools, developers must pay careful attention to how
locks are acquired and released. Otherwise, deadlock can occur, as described next.
Prior to examining how deadlock issues can be identified and managed, we first illustrate
how deadlock can occur in a multithreaded Pthread program using POSIX mutex locks.
The pthread mutex init() function initializes an unlocked mutex. Mutex locks are
acquired and released using pthread mutex lock() and pthread mutex unlock(),
respectively. If a thread attempts to acquire a locked mutex, the call to pthread mutex
lock() blocks the thread until the owner of the mutex lock invokes pthread mutex
unlock().
Two mutex locks are created and initialized in the following code example:
pthread mutex t first mutex; pthread mutex t
second mutex;
Next, two threads—thread one and thread two—are created, and both these threads
have access to both mutex locks. thread one and thread two run in the functions do
work one() and do work two(), respectively, as shown in Figure 8.1.
In this example, thread one attempts to acquire the mutex locks in the order (1) first
mutex, (2) second mutex. At the same time, thread two attempts to acquire the mutex
locks in the order (1) second mutex, (2) first mutex. Deadlock is possible if thread one
acquires first mutex while thread two acquires second mutex.
Note that, even though deadlock is possible, it will not occur if thread one can acquire and
release the mutex locks for first mutex and second mutex before thread two attempts to acquire
the locks. And, of course, the order in which the threads run depends on how they are scheduled
by the CPU scheduler. This example illustrates a problem with handling deadlocks: it is difficult
to identify and test for deadlocks that may occur only under certain scheduling circumstances.
8.2.1 Livelock
Livelock is another form of liveness failure. It is similar to deadlock; both prevent two or more
threads from proceeding, but the threads are unable to proceed for different reasons. Whereas
deadlock occurs when every thread in a set is blocked waiting for an event that can be caused
only by another thread in the set, livelockoccurs when a thread continuously attempts an action
that fails. Livelock is similar to what sometimes happens when two people attempt to pass in a
hallway: One moves to his right, the other to her left, still obstructing each other’s progress. Then
he moves to his left, and she moves
to her right, and so forth. They aren’t blocked, but they aren’t making any progress.
Livelock can be illustrated with the Pthreads pthread mutex trylock() function,
which attempts to acquire a mutex lock without blocking. The code example in Figure
8.2 rewrites the example from Figure 8.1 so that it now uses pthread mutex trylock().
This situation can lead to livelock if thread one acquires first mutex, followed by
thread two acquiring second mutex. Each thread then invokes pthread mutex
trylock(), which fails, releases their respective locks, and repeats the same actions
indefinitely.
Livelock typically occurs when threads retry failing operations at the same time.
It thus can generally be avoided by having each thread retry the failing operation at
random times. This is precisely the approach taken by Ethernet networks when a
network collision occurs. Rather than trying to retransmit a packet immediately after
a collision occurs, a host involved in a collision will backoff a random period of time
before attempting to transmit again.
Livelock is less common than deadlock but nonetheless is a challenging
issueindesigningconcurrent applications,and likedeadlock,it may only occur under
specific scheduling circumstances.
conditions are not completely independent. We shall see in Section 8.5, however, that it is
useful to consider each condition separately.
T1 T2 T3
R2
R4
• Resource instances:
◦ One instance of resource type R1
• Thread states:
◦ Thread T1 is holding an instance of resource type R2 and is waiting for an instance of
resource type R1.
Given the definition of a resource-allocation graph, it can be shown that, if the graph
contains no cycles, then no thread in the system is deadlocked. If the graph does contain a cycle,
then a deadlock may exist.
If each resource type has exactly one instance, then a cycle implies that a deadlock has
occurred. If the cycle involves only a set of resource types, each of which has only a single
instance, then a deadlock has occurred. Each thread involved in the cycle is deadlocked. In this
case, a cycle in the graph is both a necessary and a sufficient condition for the existence of
deadlock.
If each resource type has several instances, then a cycle does not necessarily imply that a
deadlock has occurred. In this case, a cycle in the graph is a necessary but not a sufficient
condition for the existence of deadlock.
To illustrate this concept, we return to the resource-allocation graph depicted in Figure 8.4.
Suppose that thread T3 requests an instance of resource type R2. Since no resource instance is
currently available, we add a request
R1 R3
T1 T2 T3
R2
R4
edge T3 → R2 to the graph (Figure 8.5). At this point, two minimal cycles exist
in the system:
T1 → R1 → T2 → R3 → T3 → R2 → T1
T2 → R3 → T3 → R2 → T2
Threads T1, T2, and T3 are deadlocked. Thread T2 is waiting for the resource R3, which is held by
thread T3. Thread T3 is waiting for either thread T1 or thread T2 to release resource R2. In addition,
thread T1 is waiting for thread T2 to release resource R1.
Now consider the resource-allocation graph in Figure 8.6. In this example, we also have a
cycle:
T1 → R 1 → T3 → R 2 → T1
T2
R1
T3
T1
R2
T4
Figure 8.6 Resource-allocation graph with a cycle but no deadlock.
However, there is no deadlock. Observe that thread T4 may release its instance of
resource type R2. That resource can then be allocated to T3, breaking the cycle.
In summary, if a resource-allocation graph does not have a cycle, then the system is
not in a deadlocked state. If there is a cycle, then the system may or may not be in a
deadlocked state. This observation is important when we deal with the deadlock
problem.
Generally speaking, we can deal with the deadlock problem in one of three ways:
• We can ignore the problem altogether and pretend that deadlocks never occur in the
system.
• We can use a protocol to prevent or avoid deadlocks, ensuring that the system will
never enter a deadlocked state.
• We can allow the system to enter a deadlocked state, detect it, and recover.
The first solution is the one used by most operating systems, including Linux and
Windows. It is then up to kernel and application developers to write programs that handle
deadlocks, typically using approaches outlined in the second solution. Some systems—
such as databases—adopt the third solution, allowing deadlocks to occur and then
managing the recovery.
Next, we elaborate briefly on the three methods for handling deadlocks. Then, in
Section 8.5 through Section 8.8, we present detailed algorithms. Before proceeding, we
should mention that some researchers have argued that none of the basic approaches
alone is appropriate for the entire spectrum of resourceallocation problems in operating
systems. The basic approaches can be combined, however, allowing us to select an
optimal approach for each class of resources in a system.
To ensure that deadlocks never occur, the system can use either a
deadlockprevention or a deadlock-avoidance scheme. Deadlock prevention provides a
set of methods to ensure that at least one of the necessary conditions (Section 8.3.1)
cannot hold. These methods prevent deadlocks by constraining how requests for
resources can be made. We discuss these methods in Section 8.5.
Deadlock avoidance requires that the operating system be given additional
information in advance concerning which resources a thread will request and use during
its lifetime. With this additional knowledge, the operating system can decide for each
request whether or not the thread should wait. To decide whether the current request can
be satisfied or must be delayed, the system must consider the resources currently
available, the resources currently allocated to each thread, and the future requests and
releases of each thread. We discuss these schemes in Section 8.6.
If a system does not employ either a deadlock-prevention or a deadlockavoidance
algorithm, then a deadlock situation may arise. In this environment, the system can
provide an algorithm that examines the state of the system to determine whether a
deadlock has occurred and an algorithm to recover from
8.5 Deadlock Prevention
the deadlock (if a deadlock has indeed occurred). We discuss these issues in Section 8.7
and Section 8.8.
In the absence of algorithms to detect and recover from deadlocks, we may arrive at
a situation in which the system is in a deadlocked state yet has no way of recognizing
what has happened. In this case, the undetected deadlock will cause the system’s
performance to deteriorate, because resources are being held by threads that cannot run
and because more and more threads, as they make requests for resources, will enter a
deadlocked state. Eventually, the system will stop functioning and will need to be
restarted manually.
Although this method may not seem to be a viable approach to the deadlock problem,
it is nevertheless used in most operating systems, as mentioned earlier. Expense is one
important consideration. Ignoring the possibility of deadlocks is cheaper than the other
approaches. Since in many systems, deadlocks occur infrequently (say, once per month),
the extra expense of the other methods may not seem worthwhile.
In addition, methods used to recover from other liveness conditions, such as livelock,
may be used to recover from deadlock. In some circumstances, a system is suffering from
a liveness failure but is not in a deadlocked state. We see this situation, for example, with
a real-time thread running at the highest priority (or any thread running on a
nonpreemptive scheduler) and never returning control to the operating system. The
system must have manual recovery methods for such conditions and may simply use
those techniques for deadlock recovery.
As we noted in Section 8.3.1, for a deadlock to occur, each of the four necessary
conditions must hold. By ensuring that at least one of these conditions cannot hold, we
can prevent the occurrence of a deadlock. We elaborate on this approach by examining
each of the four necessary conditions separately.
8.5.3 No Preemption
The third necessary condition for deadlocks is that there be no preemption of resources that have
already been allocated. To ensure that this condition does not hold, we can use the following
protocol. If a thread is holding some resources and requests another resource that cannot be
immediately allocated to it (that is, the thread must wait), then all resources the thread is currently
holding are preempted. In other words, these resources are implicitly released. The preempted
resources are added to the list of resources for which the thread is waiting. The thread will be
restarted only when it can regain its old resources, as well as the new ones that it is requesting.
Alternatively, if a thread requests some resources, we first check whether they are available.
If they are, we allocate them. If they are not, we check whether they are allocated to some other
thread that is waiting for additional resources. If so, we preempt the desired resources from the
waiting thread and allocate them to the requesting thread. If the resources are neither available
nor held by a waiting thread, the requesting thread must wait. While it is waiting, some of its
resources may be preempted, but only if another thread requests them. A thread can be restarted
only when it is allocated the new resources it is requesting and recovers any resources that were
preempted while it was waiting.
This protocol is often applied to resources whose state can be easily saved and restored later,
such as CPU registers and database transactions. It cannot generally be applied to such resources
as mutex locks and semaphores, precisely the type of resources where deadlock occurs most
commonly.
compare two resources and to determine whether one precedes another in our ordering. Formally,
we define a one-to-one function F: R → N, where N is the set of natural numbers. We can
accomplish this scheme in an application program by developing an ordering among all
synchronization objects in the system. For example, the lock ordering in the Pthread program
shown in Figure 8.1 could be
F(first mutex) = 1
F(second mutex) = 5
We can now consider the following protocol to prevent deadlocks: Each thread can request
resources only in an increasing order of enumeration. That is, a thread can initially request an
instance of a resource—say, Ri. After that, the thread can request an instance of resource Rj if and
only if F(Rj) > F(Ri). For example, using the function defined above, a thread that wants to use
both first mutex and second mutex at the same time must first request first mutex and then
second mutex. Alternatively, we can require that a thread requesting an instance of resource Rj
must have released any resources R such that F(R ) ≥ F(R ). Note also that if several instances of
i i j
the same resource type are needed, a single request for all of them must be issued.
If these two protocols are used, then the circular-wait condition cannot hold. We can
demonstrate this fact by assuming that a circular wait exists (proof by contradiction). Let the set
of threads involved in the circular wait be {T0, T1, ..., Tn}, where Ti is waiting for a resource Ri,
which is held by thread Ti+1. (Modulo arithmetic is used on the indexes, so that Tn is waiting for a
resource Rn held by T0.) Then, since thread Ti+1 is holding resource Ri while requesting resource
Ri+1, we must have F(Ri) < F(Ri+1) for all i. But this condition means that F(R0) < F(R1) < ... <
F(Rn) < F(R0). By transitivity, F(R0) < F(R0), which is impossible. Therefore, there can be no
circular wait.
Keep in mind that developing an ordering, or hierarchy, does not in itself prevent deadlock.
It is up to application developers to write programs that follow the ordering. However,
establishing a lock ordering can be difficult, especially on a system with hundreds—or
thousands—of locks. To address this challenge, many Java developers have adopted the strategy
of using the method System.identityHashCode(Object) (which returns the default hash code
value of the Object parameter it has been passed) as the function for ordering lock acquisition.
It is also important to note that imposing a lock ordering does not guarantee deadlock
prevention if locks can be acquired dynamically. For example, assume we have a function that
transfers funds between two accounts. To prevent a race condition, each account has an associated
mutex lock that is obtained from a get lock() function such as that shown in Figure 8.7. Deadlock
is possible if two threads simultaneously invoke the transaction() function, transposing different
accounts. That is, one thread might invoke transaction(checkingaccount, savings account,
25.0)
and another might invoke transaction(savingsaccount, checking account, 50.0)
acquire(lock1); acquire(lock2);
release(lock2);
release(lock1); }
Figure 8.7 Deadlock example with lock ordering.
Although ensuring that resources are acquired in the proper order is the responsibility
of kernel and application developers, certain software can be used to verify that locks
are acquired in the proper order. To detect possible deadlocks, Linux provides
lockdep, a tool with rich functionality that can be used to verify locking order in the
kernel. lockdep is designed to be enabled on a running kernel as it monitors usage
patterns of lock acquisitions and releases against a set of rules for acquiring and
releasing locks. Two examples follow, but note that lockdep provides significantly
more functionality than what is described here:
• The order in which locks are acquired is dynamically maintained by the system.
If lockdep detects locks being acquired out of order, it reports a possible
deadlock condition.
A state is safe if the system can allocate resources to each thread (up to its maximum)
in some order and still avoid a deadlock. More formally, a system is in a safe state only
if there exists a safe sequence. A sequence of threads <T1, T2, ..., Tn> is a safe sequence
for the current allocation state if, for each Ti, the resource requests that Ti can still make
can be satisfied by the currently available resources plus the resources held by all Tj,
with j < i. In this situation, if the resources that Ti needs are not immediately available,
then Ti can wait until all Tj have finished. When they have finished, Ti can obtain all of
its
unsafe
deadlock
safe
needed resources, complete its designated task, return its allocated resources, and
terminate. When Ti terminates, Ti+1 can obtain its needed resources, and so on. If no
such sequence exists, then the system state is said to be unsafe.
A safe state is not a deadlocked state. Conversely, a deadlocked state is an unsafe
state. Not all unsafe states are deadlocks, however (Figure 8.8). An unsafe state may
lead to a deadlock. As long as the state is safe, the operating system can avoid unsafe
(and deadlocked) states. In an unsafe state, the operating system cannot prevent threads
from requesting resources in such a way that a deadlock occurs. The behavior of the
threads controls unsafe states. To illustrate, consider a system with twelve resources
and three threads: T0, T1, and T2. Thread T0 requires ten resources, thread T1 may need
as many as four, and thread T2 may need up to nine resources. Suppose that, at time t0,
thread T0 is holding five resources, thread T1 is holding two resources, and thread T2 is
holding two resources. (Thus, there are three free resources.)
At time t0, the system is in a safe state. The sequence <T1, T0, T2> satisfies the
safety condition. Thread T1 can immediately be allocated all its resources and then
return them (the system will then have five available resources); then thread T0 can
getall its resourcesand returnthem (the system willthen have ten available resources);
and finally thread T2 can get all its resources and return them (the system will then have
all twelve resources available).
Asystem can go from a safe state to an unsafe state. Suppose that, at time t1, thread
T2 requests and is allocated one more resource. The system is no longer in a safe state.
At this point, only thread T1 can be allocated all its resources. When it returns them,
the system will have only four available resources. Since thread T0 is allocated five
resources but has a maximum of ten, it may request five more resources. If it does so,
it will have to wait, because they are unavailable. Similarly, thread T2 may request six
additional resources and have to wait, resulting in a deadlock. Our mistake was in
granting the request from thread T2 for one more resource. If we had made T2 wait until
either of the other threads had finished and released its resources, then we could have
avoided the deadlock.
Given the concept of a safe state, we can define avoidance algorithms that ensure
that the system will neverdeadlock. The idea is simply to ensure that the system will
always remain in a safe state. Initially, the system is in a safe state. Whenever a thread
requests a resource that is currently available, the system must decide whether the
resource can be allocated immediately or the thread must wait. The request is granted
only if the allocation leaves the system in a safe state.
In this scheme, if a thread requests a resource that is currently available, it may
still have to wait. Thus, resource utilization may be lower than it would otherwise be.
T1 T2
R2
R1
T1 T2
R2
Figure 8.10 An unsafe state in a resource-allocation graph.
1. Let Work and Finish be vectors of length m and n, respectively. Initialize Work
= Available and Finish[i] = false for i = 0, 1, ..., n − 1.
2. Find an index i such that both
a. Finish[i] == false
b. Needi ≤ Work
Available = Available–Requesti
Allocationi = Allocationi + Requesti
Needi = Needi –Requesti
Need
ABC
T0 743
T1 122
T2 600
T3 011
T4 431
We claim that the system is currently in a safe state. Indeed, the sequence <T1, T3,
T4, T2, T0> satisfies the safety criteria. Suppose now that thread T1 requests one
additional instance of resource type A and two instances of resource type C, so
Request1 = (1,0,2). To decide whether this request can be immediately granted, we first
check that Request ≤ Available—that is, that
1
8.7 Deadlock Detection
(1,0,2) ≤ (3,3,2), which is true. We then pretend that this request has been fulfilled, and
we arrive at the following new state:
We must determine whether this new system state is safe. To do so, we execute our
safety algorithm and find that the sequence <T1, T3, T4, T0, T2> satisfies the safety
requirement. Hence, we can immediately grant the request of thread T1.
You should be able to see, however, that when the system is in this state, a request for
(3,3,0) by T4 cannot be granted, since the resources are not available. Furthermore, a
request for (0,2,0) by T0 cannot be granted, even though the resources are available, since
the resulting state is unsafe.
We leave it as a programming exercise for students to implement the banker’s
algorithm.
• An algorithm that examines the state of the system to determine whether a deadlock
has occurred
R1 R3 R4
T5
T1 T2 T3
T1 T2 T3
T4
T4
R2 R5
(a) (b)
exists in a wait-for graph if and only if the corresponding resource-allocation graph contains two
edges Ti → Rq and Rq → Tj for some resource Rq. In Figure
8.11, we present a resource-allocation graph and the corresponding wait-for graph.
As before, a deadlock exists in the system if and only if the wait-for graph contains a cycle.
To detect deadlocks, the system needs to maintain the waitfor graph and periodically invoke an
algorithm that searches for a cycle in the graph. An algorithm to detect a cycle in a graph requires
O(n2) operations, where n is the number of vertices in the graph.
The BCC toolkit described in Section 2.10.4 provides a tool that can detect potential deadlocks
with Pthreads mutex locks in a user process running on a Linux system. The BCC tool deadlock
detector operates by inserting probes which trace calls to the pthread mutex lock() and pthread
mutex unlock() functions. When the specified process makes a call to either function, deadlock
detector constructs a wait-for graph of mutex locks in that process, and reports the possibility of
deadlock if it detects a cycle in the graph.
• Available. Avector of length m indicates the number of available resources of each type.
8.7 Deadlock Detection
DEADLOCK DETECTION USING JAVATHREAD DUMPS
Although Java does not provide explicit support for deadlock detection, a thread dump can be
used to analyze a running program to determine if there is a deadlock. A thread dump is a useful
debugging tool that displays a snapshot of the states of all threads in a Java application. Java
thread dumps also show locking information, including which locks a blocked thread is waiting
to acquire. When a thread dump is generated, the JVM searches the wait-for graph to detect cycles,
reportingany deadlocks it detects. To generate a thread dump of a running application, from the
command line enter:
• Allocation. An n × m matrix defines the number of resources of each type currently allocated
to each thread. • Request. An n × m matrix indicates the current request of each thread. If
Request[i][j] equals k, then thread Ti is requesting k more instances of resource type Rj.
The ≤ relation between two vectors is defined as in Section 8.6.3. To simplify notation, we
again treat the rows in the matrices Allocation and Request as vectors; we refer to them as
Allocationi and Requesti. The detection algorithm described here simply investigates every
possible allocation sequence for the threads that remain to be completed. Compare this algorithm
with the banker’s algorithm of Section 8.6.3.
1. Let Work and Finish be vectors of length m and n, respectively. Initialize Work =
Available. For i = 0, 1, ..., n–1, if Allocationi ≠ 0, then Finish[i] = false. Otherwise,
Finish[i] = true.
2. Find an index i such that both
a. Finish[i] == false
b. Requesti ≤ Work
If no such i exists, go to step 4.
3. Work = Work + Allocationi
Finish[i] = true Go to step 2.
4. If Finish[i] == false for some i, 0 ≤ i < n, then the system is in a deadlocked state.
Moreover, if Finish[i] == false, then thread Ti is deadlocked.
This algorithm requires an order of m × n2 operations to detect whether the system is in a deadlocked
state.
You may wonder why we reclaim the resources of thread Ti (in step 3) as soon as we determine
that Request ≤ Work (in step 2b). We know that T is currently not involved in a deadlock (since
i i
Requesti ≤ Work). Thus, we take an optimistic attitude and assume that Ti will require no more
resources to complete its task; it will thus soon return all currently allocated resources to the system.
If our assumption is incorrect, a deadlock may occur later. That deadlock will be detected the next
time the deadlock-detection algorithm is invoked.
To illustrate this algorithm, we consider a system with five threads T0 through T4 and three
resource types A, B, and C. Resource type A has seven instances, resource type B has two instances,
and resource type C has six instances. The following snapshot represents the current state of the
system:
We claim that the system is not in a deadlocked state. Indeed, if we execute our algorithm, we
will find that the sequence <T0, T2, T3, T1, T4> results in Finish[i] == true for all i.
Suppose now that thread T2 makes one additional request for an instance of type C. The
Request matrix is modified as follows:
Request
ABC
T0 000
T1 202
T2 001
T3 100
T4 002
We claim that the system is now deadlocked. Although we can reclaim the resources held by
thread T0, the number of available resources is not sufficient to fulfill the requests of the other
threads. Thus, a deadlock exists, consisting of threads T1, T2, T3, and T4.
Database systems provide a useful illustration of how both open-source and commercial
software manage deadlock. Updates to a database may be performed as transactions,
and to ensure data integrity, locks are typically used. A transaction may involve several
locks, so it comes as no surprise that deadlocks are possible in a database with multiple
concurrent transactions. To manage deadlock, most transactional database systems
include a deadlock detection and recovery mechanism. The database server will
periodically search for cycles in the wait-for graph to detect deadlock among a set of
transactions. When deadlock is detected, a victim is selected and the transaction is
aborted and rolled back, releasing the locks held by the victim transaction and freeing
the remaining transactions from deadlock. Once the remaining transactions have
resumed, the aborted transaction is reissued. Choice of a victim transaction depends on
the database system; for instance, MySQL attempts to select transactions that minimize
the number of rows being inserted, updated, or deleted.
If deadlocks occur frequently, then the detection algorithm should be invoked frequently.
Resources allocated to deadlocked threads will be idle until the deadlock can be broken.
In addition, the number of threads involved in the deadlock cycle may grow.
Deadlocks occur only when some thread makes a request that cannot be granted
immediately. This request may be the final request that completes a chain of waiting
threads. In the extreme, then, we can invoke the deadlockdetection algorithm every time
a request for allocation cannot be granted immediately.In this case, we can identify not
only the deadlockedset of threads but also the specific thread that “caused” the deadlock.
(In reality, each of the deadlocked threads is a link in the cycle in the resource graph, so
all of them, jointly, caused the deadlock.) If there are many different resource types, one
request may create many cycles in the resource graph, each cycle completed by the most
recent request and “caused” by the one identifiable thread.
Of course, invoking the deadlock-detection algorithm for every resource request will
incur considerable overhead in computation time. A less expensive alternative is simply
to invoke the algorithm at defined intervals—for example, once per hour or whenever CPU
utilization drops below 40 percent. (Adeadlock eventually cripples system throughput and
causes CPU utilization to drop.) If the detection algorithm is invoked at arbitrary points in
time, the resource graph may contain many cycles. In this case, we generally cannot tell
which of the many deadlocked threads “caused” the deadlock.
When a detection algorithm determines that a deadlock exists, several alternatives are
available. One possibility is to inform the operator that a deadlock has occurred and to let
the operator deal with the deadlock manually. Another possibility is to let the system
recover from the deadlock automatically. There are two options for breaking a deadlock.
One is simply to abort one or more threads to break the circular wait. The other is to
preempt some resources from one or more of the deadlocked threads.
• Abort all deadlocked processes. This method clearly will break the deadlock cycle, but at
great expense. The deadlocked processes may have computed for a long time, and the results
of these partial computations must be discarded and probably will have to be recomputed later.
• Abort one process at a time until the deadlock cycle is eliminated. This method incurs
considerable overhead, since after each process is aborted, a deadlock-detection algorithm
must be invoked to determine whether any processes are still deadlocked.
Aborting a process may not be easy. If the process was in the midst of updating a file,
terminating it may leave that file in an incorrect state. Similarly, if the process was in the midst of
updating shared data while holding a mutex lock, the system must restore the status of the lock as
being available, although no guarantees can be made regarding the integrity of the shared data.
If the partial termination method is used, then we must determine which deadlocked process
(or processes) should be terminated. This determination is a policy decision, similar to CPU-
scheduling decisions. The question is basically an economic one; we should abort those processes
whose termination will incur the minimum cost. Unfortunately, the term minimum cost is not a
precise one. Many factors may affect which process is chosen, including:
8.9 Summary
• Deadlock occurs in a set of processes when every process in the set is waiting for an
event that can only be caused by another process in the set. • There are four necessary
conditions for deadlock: (1) mutual exclusion, (2) hold and wait, (3) no preemption,
and (4) circular wait. Deadlock is only possible when all four conditions are present.
• Deadlocks can be prevented by ensuring that one of the four necessary conditions for
deadlock cannot occur. Of the four necessary conditions, eliminating the circular wait
is the only practical approach.
• Deadlock can be avoided by using the banker’s algorithm, which does not grant
resources if doing so would lead the system into an unsafe state where deadlock
would be possible.
8.1 List three examples of deadlocks that are not related to a computersystem
environment.
8.2 Suppose that a system is in an unsafe state. Show that it is possible for the
threads to complete their execution without entering a deadlocked state.
8.5 Prove that the safety algorithm presented in Section 8.6.3 requires an order of m
× n2 operations.
8.6 Consider a computer system that runs 5,000 jobs per month and has no deadlock-
prevention or deadlock-avoidance scheme. Deadlocks occur about twice per
month, and the operator must terminate and rerun about ten jobs per deadlock.
Each job is worth about two dollars (in CPU time), and the jobs terminated tend to
be about half done when they are aborted.
A systems programmer has estimated that a deadlock-avoidance algorithm
(like the banker’s algorithm) could be installed in the system with an increase of
about 10 percent in the average execution time per
Practice Exercises
job. Since the machine currently has 30 percent idle time, all 5,000 jobs per month could still
be run, although turnaround time would increase by about 20 percent on average.
a. What are the arguments for installing the deadlock-avoidance algorithm?
b. What are the arguments against installing the deadlock-avoidance algorithm?
8.7 Can a system detect that some of its threads are starving? If you answer “yes,”
explain how it can. If you answer “no,” explain how the system can deal with the
starvation problem.
8.8 Consider the following resource-allocation policy. Requests for and releases of
resources are allowed at any time. If a request for resources cannot be satisfied
because the resources are not available, then we check any threads that are blocked
waiting for resources. If a blocked thread has the desired resources, then these
resources are taken away from it and are given to the requesting thread. The vector
of resources for which the blocked thread is waiting is increased to include the
resources that were taken away.
For example, a system has three resource types, and the vector Available is initialized to
(4,2,2). If thread T0 asks for (2,2,1), it gets them. If T1 asks for (1,0,1), it gets them. Then, if
T0 asks for (0,0,1), it is blocked (resource not available). If T2 now asks for (2,0,0), it gets the
available one (1,0,0), as well as one that was allocated to T0 (since T0 is blocked). T0’s
Allocation vector goes down to (1,2,1), and its Need vector goes up to (1,0,1).
a. Can deadlock occur? If you answer “yes,” give an example. If you answer “no,” specify
which necessary condition cannot occur.
b. Can indefinite blocking occur? Explain your answer.
Allocation Max
ABCD ABCD
T0 3014 5117
T1 2210 3211
T2 3121 3321
T3 0510 4612
T4 4212 6325
Using the banker’s algorithm, determine whether or not each of the following states is unsafe.
If the state is safe, illustrate the order in which the threads may complete. Otherwise, illustrate
why the state is unsafe.
a. Available = (0, 3, 0, 1)
b. Available = (1, 0, 0, 2)
8.10 Suppose that you have coded the deadlock-avoidance safety algorithm that
determines if a system is in a safe state or not, and now have been asked to
implement the deadlock-detection algorithm. Can you do so by simply using the
safety algorithm code and redefining Maxi = Waitingi + Allocationi, where
Waitingi is a vector specifying the resources for which thread i is waiting and
Allocationi is as defined in Section 8.6? Explain your answer.
8.11 Is it possible to have a deadlock involving only one single-threaded process?
Explain your answer.
Further Reading
Most research involving deadlock was conducted many years ago. [Dijkstra (1965)] was
one of the first and most influential contributors in the deadlock area.
Details of how the MySQL database manages deadlock can be found at
http://dev.mysql.com/.
Details on the lockdep tool can be found at https://www.kernel.org/doc/
Documentation/locking/lockdep-design.txt.
Bibliography
8.13 Draw the resource-allocation graph that illustrates deadlock from the program
example shown in Figure 8.1 in Section 8.2.
8.14 In Section 6.8.1, we described a potential deadlock scenario involving processes P0
and P1 and semaphores S and Q. Draw the resourceallocation graph that illustrates
deadlock under the scenario presented in that section.
8.15 Assume that a multithreaded application uses only reader–writer locks for
synchronization. Applying the four necessary conditions for deadlock, is deadlock
still possible if multiple reader–writer locks are used? 8.16 The program example
shown in Figure 8.1 doesn’t always lead to deadlock. Describe what role the CPU
scheduler plays and how it can contribute to deadlock in this program.
8.17 In Section 8.5.4, we described a situation in which we prevent deadlock by
ensuring that all locks are acquired in a certain order. However, we also point out
that deadlock is possible in this situation if two threads simultaneously invoke the
transaction() function. Fix the transaction() function to prevent deadlocks.
Exercises EX-28
8.18 Which of the six resource-allocation graphs shown in Figure 8.12 illustrate
deadlock? For those situations that are deadlocked, provide the cycle of threads
and resources. Where there is not a deadlock situation, illustrate the order in which
the threads may complete execution.
8.19 Compare the circular-wait scheme with the various deadlock-avoidance schemes
(like the banker’s algorithm) with respect to the following issues:
a. Runtime overhead
b. System throughput
8.20 In a real computer system, neither the resources available nor the demands of threads for
resources are consistent over long periods (months). Resources break or are replaced, new
processes and threads come and go, and new resources are bought and added to the system.
If deadlock is controlled by the banker’s algorithm, which of the
Allocation Max
ABCD ABCD
T0 2106 6327
T1 3313 5415
T2 2312 6614
T3 1234 4345
T4 3030 7261
a. The maximum need of each thread is between one resource and m resources.
b. The sum of all maximum needs is less than m + n.
8.24 Consider the version of the dining-philosophers problem in which the chopsticks are
placed at the center of the table and any two of them can be used by a philosopher.
Assume that requests for chopsticks are made one at a time. Describe a simple rule
for determining whether a particular request can be satisfied without causing
deadlock given the current allocation of chopsticks to philosophers.
8.25 Consider again the setting in the preceding exercise. Assume now that each
philosopher requires three chopsticks to eat. Resource requests are still issued one at
a time. Describe some simple rules for determining whether a particular request can
be satisfied without causing deadlock given the current allocation of chopsticks to
philosophers.
Exercises EX-30
8.26 We can obtain the banker’s algorithm for a single resource type from the general
banker’s algorithm simply by reducing the dimensionality of the various arrays by 1.
Show through an example that we cannot implement the multipleresource-type banker’s
scheme by applying the single-resource-type scheme to each resource type individually.
8.27 Consider the following snapshot of a system:
Allocation Max
ABCD ABCD
T0 1202 4316
T1 0112 2424
T2 1240 3651
T3 1201 2623
T4 1001 3112
Using the banker’s algorithm, determine whether or not each of the following states is
unsafe. If the state is safe, illustrate the order in which the threads may complete. Otherwise,
illustrate why the state is unsafe.
a. Available = (2,2,2,3)
b. Available = (4,4,1,1)
c. Available = (3,0,1,4)
d. Available = (1,5,2,2)
8.28 Consider the following snapshot of a system:
Programming Problems
8.32 Implement your solution to Exercise 8.30 using POSIX synchronization. In particular,
represent northbound and southbound farmers as separate threads. Once a farmer is
on the bridge, the associated thread will sleep for a random period of time,
representing traveling across the bridge. Design your program so that you can create
several threads representing the northbound and southbound farmers.
8.33 In Figure 8.7, we illustrate a transaction() function that dynamically acquires locks. In
the text, we describe how this function presents difficulties for acquiring locks in a
way that avoids deadlock. Using the Java implementation of transaction() that is
provided in the source-code download for this text, modify it using the
System.identityHashCode() method so that the locks are acquired in order.
Programming Projects
Banker’s Algorithm
For this project, you will write a program that implements the banker’s algorithm discussed
in Section 8.6.3. Customers request and release resources from the bank. The banker will
grant a request only if it leaves the system in a safe state. A request that leaves the system in
an unsafe state will be denied. Although the code examples that describe this project are
illustrated in C, you may also develop a solution using Java. The Banker
The banker will consider requests from n customers for m resources types, as outlined in
Section 8.6.3. The banker will keep track of the resources using the following data
structures:
The banker will grant a request if it satisfies the safety algorithm outlined in Section 8.6.3.1. If a request does
not leave the system in a safe state, the banker will deny it. Function prototypes for requesting and releasing
resources are as follows:
int request resources(int customer num, int request[]); void release resources(int customer
./a.out 10 5 7 8
where each line in the input file represents the maximum request of each resource type for each customer. Your
program will initialize the maximum array to these values.
Your program will then have the user enter commands responding to a request of resources, a release of
resources, or the current values of the different data structures. Use the command ‘RQ’ for requesting resources,
‘RL’ for releasing resources, and ‘*’ to output the values of the different data structures. For example, if customer
0 were to request the resources (3, 1, 2, 1), the following command would be entered:
RQ 0 3 1 2 1
Your program would then output whether the request would be satisfied or denied using the safety algorithm
outlined in Section 8.6.3.1.
P-47 Chapter 8 Deadlocks
Similarly, if customer 4 were to release the resources (1, 2, 3, 1), the user would enter the following
command:
RL 4 1 2 3 1
Finally, if the command ‘*’ is entered, your program would output the values of the available, maximum,
allocation, and need arrays.