Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 22

Understanding Deadlock Conditions

Recommended resources

Hello! Welcome to the first lesson in our module on deadlock.

In the previous modules, you've built a solid toolkit of synchronization primitives like mutexes, read-write locks, and semaphores. These tools are essential for protecting shared data. However, using them, especially when a thread needs to acquire more than one lock, introduces one of the most notorious problems in concurrent programming: deadlock.

This lesson provides the theoretical foundation you need to understand, and later prevent, this issue. Our goal is to describe the four necessary conditions for deadlock to occur. If you can understand these conditions, you can learn to write code that breaks at least one of them, making deadlock impossible.

What is a Deadlock?

At its core, a deadlock is a state where a group of threads is stuck, and no thread can proceed because each is waiting for a resource held by another thread in the same group. This creates a circular dependency that can freeze parts of your application, or the entire thing.

To build your intuition, let's start with a few analogies.

L-4.1: DEADLOCK concept | Example | Necessary condition | Operating System

The video "L-4.1: DEADLOCK concept" from Gate Smashers provides some excellent, easy-to-understand analogies for deadlock before moving into a technical example. This will help ground the concept.

Watch the video from the beginning until the 5:35 mark. First, focus on the real-world examples (the bank account and the cars) to get a feel for the general problem (0:00 - 2:10). Then, pay close attention to the technical example involving two processes (P1, P2) and two resources (R1, R2). This maps directly to the kind of scenarios we'll encounter with threads and mutexes (2:10 - 5:35).

The video's technical example is key:

  • Process P1 holds Resource R1 and waits for R2.
  • Process P2 holds Resource R2 and waits for R1.

Neither can proceed. This is often called a "deadly embrace."


The Four Necessary Conditions for Deadlock

For a deadlock to occur, four specific conditions must be met simultaneously. These are often called the Coffman conditions, named after their description in a 1971 paper by Edward G. Coffman, Jr. If you can ensure that at least one of these conditions is never met in your system, you can prevent deadlocks.

Conditions for Deadlock in Operating System

The article "Conditions for Deadlock in Operating System" from GeeksForGeeks provides a clear and concise explanation of these four conditions.

Please read the section titled "Necessary Conditions for the Occurrence of a Deadlock", which covers all four points. As you read, think about how each condition relates to the pthread_mutex_t you've already used.

Let's break down each condition and connect it to your C programming context.

1. Mutual Exclusion

  • Definition: At least one resource must be non-shareable, meaning only one thread can use it at a time.
  • In Practice: This condition is inherent to the tools we use for protecting critical sections. A pthread_mutex_t is, by definition, a mutual exclusion lock. When you call pthread_mutex_lock, you are establishing this condition for the resource the mutex protects. For many resources (like a specific memory location or I/O device), mutual exclusion is unavoidable.

2. Hold and Wait

  • Definition: A thread is holding at least one resource while simultaneously waiting to acquire additional resources held by other threads.
  • In Practice: This happens anytime a thread successfully locks one mutex and then blocks while trying to lock a second one. For example:
    // Thread 1
    pthread_mutex_lock(&mutex_A);
    // ... is now 'holding' mutex_A
    pthread_mutex_lock(&mutex_B); // Tries to lock mutex_B, may block. Now 'waiting'.
    
    If Thread 2 holds mutex_B at this moment, Thread 1 is in a state of "hold and wait."

3. No Preemption

  • Definition: A resource cannot be forcibly taken away from the thread that holds it. The thread must release it voluntarily.
  • In Practice: The Pthreads API enforces this. There is no pthread_mutex_force_unlock() function that lets one thread break another thread's lock. A lock acquired by a thread with pthread_mutex_lock can only be released by the same thread calling pthread_mutex_unlock. The operating system scheduler will not preempt a thread and take its locks away.

4. Circular Wait

  • Definition: A set of threads {T1, T2, ..., Tn} exists where T1 is waiting for a resource held by T2, T2 is waiting for a resource held by T3, and so on, until Tn is waiting for a resource held by T1.
  • In Practice: This is the condition that creates the "deadly embrace." It's the cycle of dependencies that brings everything to a halt. The simplest case involves two threads and two mutexes:
    • Thread 1: Locks mutex_A, then tries to lock mutex_B.
    • Thread 2: Locks mutex_B, then tries to lock mutex_A.

If the scheduler interleaves their execution just right, Thread 1 will hold A and wait for B, while Thread 2 holds B and waits for A. A perfect circle of waiting is formed.

Conditions for Deadlock with Circular Wait Diagram
This diagram summarizes the four conditions. The illustration on the right perfectly depicts a circular wait between two processes and two resources, which is the most common and intuitive deadlock scenario.
Test your understanding!

Consider a system with two threads, T_alpha and T_beta, and two mutexes, M1 and M2.

The code for T_alpha is:

pthread_mutex_lock(&M1);
// do some work...
pthread_mutex_lock(&M2);
// do more work...
pthread_mutex_unlock(&M2);
pthread_mutex_unlock(&M1);

The code for T_beta is:

pthread_mutex_lock(&M2);
// do some work...
pthread_mutex_lock(&M1);
// do more work...
pthread_mutex_unlock(&M1);
pthread_mutex_unlock(&M2);

If T_alpha successfully locks M1 and is then preempted by the OS scheduler just before it can lock M2, and T_beta then runs and successfully locks M2, what happens next? Map this situation to the four conditions of deadlock.

Show answer

When T_beta tries to lock M1, it will block because T_alpha holds it. When T_alpha resumes, it will try to lock M2 and block because T_beta holds it. A deadlock occurs.

Here is how the four conditions are met:

  1. Mutual Exclusion: M1 and M2 are mutexes, so they can only be held by one thread at a time. This condition is met.
  2. Hold and Wait: T_alpha holds M1 and is waiting for M2. T_beta holds M2 and is waiting for M1. This condition is met for both threads.
  3. No Preemption: T_alpha cannot have M1 taken from it, and T_beta cannot have M2 taken from it. They must be unlocked voluntarily. This condition is met.
  4. Circular Wait: T_alpha is waiting for a resource (M2) held by T_beta, and T_beta is waiting for a resource (M1) held by T_alpha. This forms a cycle of length two. This condition is met.

Since all four conditions are met, deadlock is inevitable.


The Dining Philosophers: A Classic Deadlock Problem

To solidify the concept of circular wait, let's look at a classic computer science problem that illustrates it perfectly: the Dining Philosophers.

Deadlock 3: Dining Philosophers

The video "Deadlock 3: Dining Philosophers" by Jacob Schrum provides an excellent visualization of this problem. It shows how a seemingly reasonable strategy for resource acquisition leads directly to a circular wait and deadlock.

Please watch two segments of this video: 0:00 - 1:53: Understand the problem setup. Note how the first three conditions (Mutual Exclusion, Hold and Wait, No Preemption) are naturally part of the problem's rules. 4:56 - 7:33: This part shows how a simple rule—"each philosopher picks up their right fork first"—inevitably leads to a circular wait and thus a deadlock. The resource allocation graph makes the cycle visually obvious.

The Dining Philosophers problem is a powerful mental model to keep in mind. It demonstrates how a system of independent actors, each following a simple and locally-sensible rule, can collectively create a global system failure.


Conclusion

In this lesson, we've defined deadlock and laid out the four conditions that must exist simultaneously for it to occur. Understanding these is the first, critical step toward writing deadlock-free concurrent code.

Key Takeaways:

  • A deadlock is a state where a group of threads is permanently blocked, each waiting for a resource held by another in the group.
  • For a deadlock to happen, all four Coffman conditions must be true:
    1. Mutual Exclusion: Resources are non-shareable (e.g., a mutex).
    2. Hold and Wait: A thread holds one resource while waiting for another.
    3. No Preemption: Resources cannot be forcibly taken away.
    4. Circular Wait: A closed chain of threads exists, each waiting for a resource held by the next thread in the chain.

Preview of the Next Lesson:
Knowing the causes is only half the battle. The real goal is prevention. Since all four conditions are necessary for deadlock, we can prevent it by breaking just one of them. In our next lesson, we will focus on the most common and practical strategy: breaking the circular wait condition by implementing a strict lock-ordering policy. This technique will allow you to safely acquire multiple mutexes without risking a deadly embrace.