Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Operating Systems  Module 6 – Deadlocks

Deadlock: conditions and resource graphs

What a deadlock is, the four Coffman conditions it needs, how resource-allocation and wait-for graphs expose one, and how livelock and starvation differ.

  • Intermediate
  • 20 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Clang 21.0.0 and GCC 16.2.0
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • State the four Coffman conditions and show that a deadlock needs all four
  • Draw resource-allocation and wait-for graphs and find the cycle in them
  • Decide whether a cycle means deadlock when a resource has several instances
  • Distinguish deadlock, livelock and starvation
  • Recognise the deadlocks that hide in lock order, callbacks and full thread pools

Before you start

What you’ll learn · Before you start

What you will learn

  • State the four Coffman conditions and show that a deadlock needs all four
  • Draw resource-allocation and wait-for graphs and find the cycle in them
  • Decide whether a cycle means deadlock when a resource has several instances
  • Distinguish deadlock, livelock and starvation
  • Recognise the deadlocks that hide in lock order, callbacks and full thread pools

Before you start

On this page

Two threads move money between accounts 17 and 42 at the same moment: one locks account 17 and then 42, the other locks 42 and then 17. In the run below each took its first lock and then waited a full second for the other’s before giving up, and without that deadline both would still be waiting. That is a deadlock: A state in which every thread of a group waits for something that only another thread of the same group can do, such as releasing a lock, so none of them can ever go on.: every thread of a group waits for something that only another thread of the same group can do.

Deadlocks follow rules. Four conditions hold whenever one exists, a graph of who holds what and who waits for what shows it as a cycle, and a few code patterns produce nearly all of them. By the end of this page you can take any snapshot of threads and locks, name the threads that can never finish, and say which condition to break.

Two locks taken in opposite orders deadlock

two_locks.c runs the two transfers twice. In the first run a small meeting point makes each thread take its first lock before either thread asks for its second, the unlucky timing that a busy server reaches sooner or later. Each thread then asks for its second lock with pthread_mutex_trylock, which returns at once when the lock is taken, every millisecond for one second. In the second run both threads lock account 17 first:

two_locks.c: the same two transfers, in opposite orders and in one order C · two-locks/two_locks.c
// Two threads move money between accounts 17 and 42. When they take the two
// locks in opposite orders they deadlock; when both take lock 17 first, never.
#include <pthread.h>
#include <sched.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdio.h>
#include <time.h>

static pthread_mutex_t locks[2] = {PTHREAD_MUTEX_INITIALIZER,
                                   PTHREAD_MUTEX_INITIALIZER};
static const int account[2] = {17, 42};
static atomic_int holding, tried; // threads past each meeting point

typedef struct {
  const char *name;
  int first, second; // indexes into locks
  bool meet;         // wait until both threads hold their first lock
  bool done;
} transfer;

static void meet(atomic_int *count) {
  atomic_fetch_add(count, 1);
  while (atomic_load(count) < 2)
    sched_yield();
}

// Tries the lock every millisecond for a second instead of waiting for ever.
static bool lock_within_a_second(pthread_mutex_t *m) {
  for (int i = 0; i < 1000; i++) {
    if (pthread_mutex_trylock(m) == 0)
      return true;
    nanosleep(&(struct timespec){.tv_nsec = 1000000}, NULL);
  }
  return false;
}

static void *run(void *arg) {
  transfer *t = arg;
  pthread_mutex_lock(&locks[t->first]);
  if (t->meet)
    meet(&holding);
  t->done = lock_within_a_second(&locks[t->second]);
  if (t->meet)
    meet(&tried); // neither lets go before both have tried
  if (t->done)
    pthread_mutex_unlock(&locks[t->second]);
  pthread_mutex_unlock(&locks[t->first]);
  return NULL;
}

static void race(const char *title, transfer pair[2]) {
  pthread_t threads[2];
  for (int i = 0; i < 2; i++)
    pthread_create(&threads[i], NULL, run, &pair[i]);
  for (int i = 0; i < 2; i++)
    pthread_join(threads[i], NULL);
  puts(title);
  for (int i = 0; i < 2; i++) {
    const transfer *t = &pair[i];
    if (t->done)
      printf("  %s locked %d, then %d: transfer done\n", t->name,
             account[t->first], account[t->second]);
    else
      printf("  %s locked %d, then waited 1 s for %d: gave up\n", t->name,
             account[t->first], account[t->second]);
  }
}

int main(void) {
  race("opposite orders, both first locks taken before either second one:",
       (transfer[2]){{"A", 0, 1, true, false}, {"B", 1, 0, true, false}});
  race("one order, lock 17 first in both threads:",
       (transfer[2]){{"A", 0, 1, false, false}, {"B", 0, 1, false, false}});
  return 0;
}

Output

opposite orders, both first locks taken before either second one:
  A locked 17, then waited 1 s for 42: gave up
  B locked 42, then waited 1 s for 17: gave up
one order, lock 17 first in both threads:
  A locked 17, then 42: transfer done
  B locked 17, then 42: transfer done

Recorded with Apple clang version 21.0.0 (clang-2100.3.34.2) on macOS 26 arm64. To run it yourself: clang -std=c23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined two_locks.c -o two_locks && ./two_locks

In the first run both threads gave up. For a whole second each held one lock and waited for the other, and neither could go on, because each was waiting for a lock that only the other could release and neither releases a lock while it waits. Taking both locks in one order made the same transfers finish. A plain pthread_mutex_lock has no deadline at all: it would have waited for ever. POSIX offers pthread_mutex_timedlock and pthread_mutex_clocklock, which wait until a deadline and then return ETIMEDOUT.

Dijkstra called this state a deadly embrace. Step through the first run, drawn the way the rest of this lesson draws every snapshot, with an arrow for each lock a thread holds and a dashed arrow for each lock it wants:

Interactive · step by step Two transfers, two locks, opposite orders Step through the run of two_locks.c as a resource-allocation graph. An arrow from a lock to a thread means the thread holds it; a dashed arrow from a thread to a lock is a request.
Two transfers, two locks, opposite orders, step 7 of 7. Showing Thread A (gave up after 1 s), Lock of account 42 (free), Lock of account 17 (free), Thread B (gave up after 1 s).Thread Agave up after 1 sLock of account 42freeLock of account 17freeThread Bgave up after 1 s

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. Two transfers start. Thread A moves money from account 17 to account 42, and thread B moves money the other way. Each transfer needs both accounts' locks, and each thread locks the account it takes money from first.
  2. A locks account 17. A takes the lock of account 17. In a resource-allocation graph, an arrow from a resource to a thread is an assignment edge, which says that the thread holds it.
  3. B locks account 42. At the same moment B takes the lock of account 42. Both first locks are now taken, which is the interleaving the program forces before either thread asks for its second lock.
  4. A asks for lock 42. A needs the lock of account 42 too. An arrow from a thread to a resource is a request edge, which says that the thread waits for it. A sleeps until lock 42 is free and keeps lock 17 meanwhile.
  5. B asks for lock 17. B needs the lock of account 17, which A holds. The four arrows now form a loop, A to lock 42 to B to lock 17 and back to A.
  6. A cycle, so nobody moves. Follow any arrow and you come back to where you started. Each thread waits for a lock that only the other thread can release, and neither releases while it waits, so both would wait for ever.
  7. Both give up after a second. In two_locks.c each thread tries its second lock for one second, then gives up and releases its first lock, which ends the wait by dropping the transfer. A plain lock call has no such deadline.

Four arrows were enough: each thread held one lock and wanted the other’s, and following the arrows led back to the start. The next sections turn that loop into a rule you can check.

A deadlock needs all four Coffman conditions

In 1971 Coffman, Elphick and Shoshani described four conditions that hold whenever a deadlock exists, now called the Coffman conditions: The four conditions that hold whenever a deadlock exists: mutual exclusion, hold and wait, no preemption and circular wait. Removing any one of them prevents deadlock.. All four are visible in the first run of two_locks.c:

Condition In the first run of two_locks.c Without it
mutual exclusion: The guarantee that at most one thread is inside a given critical section at any moment. More in “Race conditions and the critical-section problem” A mutex has one owner at a time, so lock 17 cannot serve A and B together A resource that every thread may use at once makes nobody wait for it
hold and wait: A condition for deadlock: a thread keeps the resources it already holds while it waits for another one. A keeps lock 17 while it waits for lock 42 A thread that waits while holding nothing blocks no other thread
No preemption Nothing can take lock 17 away from A, and only a mutex’s owner may release it The system could take one lock back from a waiting thread and let the other finish
circular wait: A condition for deadlock: a closed chain of threads in which each one waits for a resource that the next one holds. A waits for B, and B waits for A Some waiting thread waits for a thread that is not waiting, which finishes and lets the chain move

The last column is the proof that a deadlock needs all four. Take away mutual exclusion and nobody waits; take away hold and wait and a waiting thread is no obstacle to anyone; allow preemption and the system can break the wait by force. Take away circular wait and every chain of waiting threads ends at a thread that is not waiting: that thread runs to its end and releases what it holds, then the thread behind it can go on, and so on down the chain.

The first three conditions describe how resources are handed out, and ordinary locks meet all three. Circular wait describes the state that the threads reach, which is why the second run worked: when every thread takes the two locks in the same order, no thread can hold the later lock while waiting for the earlier one, so no cycle can form. When each resource has one instance, the four conditions are also enough: a circular wait among such locks is a deadlock.

Key idea

A deadlock needs mutual exclusion, hold and wait, no preemption and a circular wait at the same time. Take away any one of them and the waiting can always end.

Checkpoint

Check: Deadlock from two locks taken in opposite orders · The four Coffman conditions

  1. Question 1 of 2 In the first run of two_locks.c, why could neither thread go on, without the one-second deadline?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Each waited for a lock that only the other could release, and neither released a lock while it waited

    A deadlock is a group in which every thread waits for something only another member can do. Here the group is A and B, and the something is releasing the lock that each one holds.

  2. Question 2 of 2 In the second run of two_locks.c both threads lock account 17 first. Which Coffman condition can then no longer hold?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Circular wait

    Taking locks in one global order leaves mutual exclusion, hold and wait and no preemption in place and makes a cycle impossible, which is why the same transfers finished.

A resource-allocation graph shows who holds and who waits

The picture in the first section is a resource-allocation graph: A graph of threads and resources in which an arrow from a thread to a resource is a request and an arrow from a resource to a thread means that the thread holds it., the graph model that Holt described in 1972. Each thread is a circle and each resource a box with one dot per instance. An arrow from a thread to a resource is a request, the thread waits for it; an arrow from a resource to a thread means the thread holds it. When every resource has a single instance, each request points through the resource at exactly one holder, and the resources can be left out: an arrow from thread A to thread B then says that A waits for a lock B holds. That smaller drawing is the wait-for graph: A graph with an arrow from each waiting thread to the thread that holds what it waits for. When every resource has one instance, a cycle in it is a deadlock., and its cycles are the cycles of the resource-allocation graph.

A server snapshot with five threads and four locks:

Thread Holds Waits for So it waits for
A log cache B
B cache index C
C index log A
D nothing stats E
E stats nothing nobody

A cycle is easy to see among five threads and hard to see among five hundred, so a program looks for it the other way round: it finishes every thread that can finish. A thread that waits for nobody can run to its end and release its locks, and then every thread that waited only for it can run too. Whatever never finishes is deadlocked.

The figure runs that idea on the server as Kahn’s topological sort. Each arrow points from a thread to the thread waiting for it, so read it as “finishes before”. The label “in 1” means the thread still waits for one other thread, and a thread with no arrow left pointing in is ready to finish:

Interactive · step through it Finishing the server's threads one by one Repeatedly take a node with nothing pointing into it.
Graph of 5 nodes and 4 edges. ready (in-degree 0): E.Ain 1Bin 1Cin 1Din 1Ein 0ready (in-degree 0):Eorder:empty
  • being compared or looked at
  • being moved or written
  • in its final place
  • the key, pivot or target
  • visited or ruled out

Pseudo-code

  1. indeg(v) ← the number of edges into v
  2. queue ← every node with indeg 0
  3. while the queue is not empty:
  4. u ← dequeue; append u to the order
  5. for each edge u → v: indeg(v) ← indeg(v) − 1; if it is 0: enqueue v
  6. if the order misses a node: the graph has a cycle

Order the tasks so that every arrow points forwards. "in k" is a node's in-degree: how many arrows still point into it. Nodes with nothing left pointing in are ready: E.

When it finishes: No order: the graph has a cycle.

Placed 0
Edges removed 0

E waits for nobody, so it finishes and releases the stats lock, and then D can take it and finish. A, B and C each wait for the next one, so none of them ever becomes ready, and the run ends with the three of them on a cycle: they are the deadlocked threads. The program below does the same for four snapshots, in 14 languages. It reads each one as its wait-for graph, the last column of the table, and once nothing more can finish it follows the waits from the first stuck thread until a thread repeats, and prints that cycle:

One program · 14 languages Finishing every thread that can finish, in four snapshots O(n²) steps for n threads, as written

Python 3.14 · main.py

# Each thread and the thread it waits for (None: nobody).
SERVER = [("A", "B"), ("B", "C"), ("C", "A"), ("D", "E"), ("E", None)]
SNAPSHOTS = [
    ("opposite orders", [("A", "B"), ("B", "A")]),
    ("same order", [("A", None), ("B", "A")]),
    ("server", SERVER),
    ("server and F", SERVER + [("F", "A")]),
]


def analyse(waits: dict[str, str | None]) -> None:
    # A thread that waits for nobody can finish and release its locks.
    ready = [t for t, holder in waits.items() if holder is None]
    finished: list[str] = []
    while ready:
        done = ready.pop(0)
        finished.append(done)
        ready += [t for t, holder in waits.items() if holder == done]
    stuck = [t for t in waits if t not in finished]
    print("  can finish:", ", ".join(finished) or "none")
    if not stuck:
        print("  deadlocked: none")
        return
    # Every stuck thread waits for another stuck thread, so following them must loop.
    path = [stuck[0]]
    while waits[path[-1]] not in path:
        path.append(waits[path[-1]])
    loop = path[path.index(waits[path[-1]]) :]
    print(f"  deadlocked: {', '.join(stuck)} (cycle {' -> '.join(loop + loop[:1])})")


for title, threads in SNAPSHOTS:
    print(title)
    analyse(dict(threads))

In Python: dict(threads) keeps the snapshot's order, so the threads that can finish are found in the order the table lists them, and waits[path[-1]] reads who the last thread on the path waits for.

Recorded with CPython 3.14.8 on macOS 26 arm64. It runs in your browser with Pyodide 314.0.7 (CPython 3.14.2). To run it yourself: python3 main.py

JavaScript Node 24 · main.mjs

// Each thread and the thread it waits for (null: nobody).
const SERVER = { A: "B", B: "C", C: "A", D: "E", E: null };
const SNAPSHOTS = {
  "opposite orders": { A: "B", B: "A" },
  "same order": { A: null, B: "A" },
  server: SERVER,
  "server and F": { ...SERVER, F: "A" },
};

function analyse(waits) {
  // A thread that waits for nobody can finish and release its locks.
  const ready = [...waits.keys()].filter((t) => waits.get(t) === null);
  const finished = [];
  while (ready.length > 0) {
    const done = ready.shift();
    finished.push(done);
    ready.push(...[...waits.keys()].filter((t) => waits.get(t) === done));
  }
  const stuck = [...waits.keys()].filter((t) => !finished.includes(t));
  console.log(`  can finish: ${finished.join(", ") || "none"}`);
  if (stuck.length === 0) {
    console.log("  deadlocked: none");
    return;
  }
  // Every stuck thread waits for another stuck thread, so following them must loop.
  const path = [stuck[0]];
  const next = () => waits.get(path[path.length - 1]);
  while (!path.includes(next())) path.push(next());
  const loop = path.slice(path.indexOf(next()));
  console.log(`  deadlocked: ${stuck.join(", ")} (cycle ${[...loop, loop[0]].join(" -> ")})`);
}

for (const [title, threads] of Object.entries(SNAPSHOTS)) {
  console.log(title);
  analyse(new Map(Object.entries(threads)));
}

In JavaScript: Object.entries turns each snapshot into a Map in the order its keys were written; a thread name like "A" is not an integer key, so that order is kept.

Recorded with Node.js 24.21.0 LTS (V8 13.6) on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0). To run it yourself: node main.mjs

Java 25 LTS · Main.java

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class Main {
  static void analyse(Map<String, String> waits) {
    // A thread that waits for nobody can finish and release its locks.
    List<String> ready = new ArrayList<>();
    waits.forEach(
        (t, holder) -> {
          if (holder == null) ready.add(t);
        });
    List<String> finished = new ArrayList<>();
    while (!ready.isEmpty()) {
      String done = ready.remove(0);
      finished.add(done);
      waits.forEach(
          (t, holder) -> {
            if (done.equals(holder)) ready.add(t);
          });
    }
    List<String> stuck = new ArrayList<>(waits.keySet());
    stuck.removeAll(finished);
    System.out.println(
        "  can finish: " + (finished.isEmpty() ? "none" : String.join(", ", finished)));
    if (stuck.isEmpty()) {
      System.out.println("  deadlocked: none");
      return;
    }
    // Every stuck thread waits for another stuck thread, so following them must loop.
    List<String> path = new ArrayList<>(List.of(stuck.get(0)));
    while (!path.contains(waits.get(path.get(path.size() - 1)))) {
      path.add(waits.get(path.get(path.size() - 1)));
    }
    String start = waits.get(path.get(path.size() - 1));
    List<String> loop = new ArrayList<>(path.subList(path.indexOf(start), path.size()));
    loop.add(start);
    System.out.println(
        "  deadlocked: " + String.join(", ", stuck) + " (cycle " + String.join(" -> ", loop) + ")");
  }

  /** Each thread and the thread it waits for (null: nobody). */
  static Map<String, String> threads(String... pairs) {
    Map<String, String> waits = new LinkedHashMap<>();
    for (int i = 0; i < pairs.length; i += 2) waits.put(pairs[i], pairs[i + 1]);
    return waits;
  }

  public static void main(String[] args) {
    String[] server = {"A", "B", "B", "C", "C", "A", "D", "E", "E", null};
    Map<String, Map<String, String>> snapshots = new LinkedHashMap<>();
    snapshots.put("opposite orders", threads("A", "B", "B", "A"));
    snapshots.put("same order", threads("A", null, "B", "A"));
    snapshots.put("server", threads(server));
    Map<String, String> withF = threads(server);
    withF.put("F", "A");
    snapshots.put("server and F", withF);
    snapshots.forEach(
        (title, waits) -> {
          System.out.println(title);
          analyse(waits);
        });
  }
}

In Java: A LinkedHashMap keeps the insertion order that the output depends on, and done.equals(holder) is called on done because holder is null for a thread that waits for nobody.

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. It runs in your browser with a TeaVM 0.16.0 build in QuickJS (quickjs-emscripten 0.32.0). To run it yourself: java Main.java

C++23 · main.cpp

#include <algorithm>
#include <optional>
#include <print>
#include <string>
#include <utility>
#include <vector>

// Each thread and the thread it waits for (std::nullopt: nobody).
using Threads = std::vector<std::pair<char, std::optional<char>>>;

std::string join(const std::string &names, const char *sep) {
  std::string out;
  for (const char t : names)
    out += (out.empty() ? "" : sep) + std::string(1, t);
  return out;
}

void analyse(const Threads &threads) {
  // A thread that waits for nobody can finish and release its locks.
  std::string ready, finished, stuck;
  for (const auto &[t, holder] : threads)
    if (!holder)
      ready += t;
  while (!ready.empty()) {
    const char done = ready.front();
    ready.erase(0, 1);
    finished += done;
    for (const auto &[t, holder] : threads)
      if (holder == done)
        ready += t;
  }
  for (const auto &[t, holder] : threads)
    if (!finished.contains(t))
      stuck += t;
  std::println("  can finish: {}",
               finished.empty() ? "none" : join(finished, ", "));
  if (stuck.empty()) {
    std::println("  deadlocked: none");
    return;
  }
  // Every stuck thread waits for another stuck thread, so following them must
  // loop.
  auto holder_of = [&](char t) {
    return std::ranges::find(threads, t, &Threads::value_type::first)
        ->second.value();
  };
  std::string path(1, stuck.front());
  while (!path.contains(holder_of(path.back())))
    path += holder_of(path.back());
  std::string loop = path.substr(path.find(holder_of(path.back())));
  loop += loop.front();
  std::println("  deadlocked: {} (cycle {})", join(stuck, ", "),
               join(loop, " -> "));
}

int main() {
  const Threads server = {
      {'A', 'B'}, {'B', 'C'}, {'C', 'A'}, {'D', 'E'}, {'E', std::nullopt}};
  Threads with_f = server;
  with_f.push_back({'F', 'A'});
  const std::vector<std::pair<std::string, Threads>> snapshots = {
      {"opposite orders", {{'A', 'B'}, {'B', 'A'}}},
      {"same order", {{'A', std::nullopt}, {'B', 'A'}}},
      {"server", server},
      {"server and F", with_f}};
  for (const auto &[title, threads] : snapshots) {
    std::println("{}", title);
    analyse(threads);
  }
}

In C++: Thread names are single chars, so a std::string holds each list of threads and contains() and find() search it; std::optional<char> marks a thread that waits for nobody.

Recorded with Apple clang 21.0.0 (C++23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang++ -std=c++23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.cpp -o main && ./main

C# 14 · .NET 10 · app.cs

// Each thread and the thread it waits for (null: nobody).
(string Thread, string? Holder)[] server =
    [("A", "B"), ("B", "C"), ("C", "A"), ("D", "E"), ("E", null)];
(string Title, (string Thread, string? Holder)[] Threads)[] snapshots =
[
    ("opposite orders", [("A", "B"), ("B", "A")]),
    ("same order", [("A", null), ("B", "A")]),
    ("server", server),
    ("server and F", [.. server, ("F", "A")]),
];
foreach (var (title, threads) in snapshots)
{
    Console.WriteLine(title);
    Analyse(threads);
}

static void Analyse((string Thread, string? Holder)[] threads)
{
    // A thread that waits for nobody can finish and release its locks.
    var ready = new Queue<string>(threads.Where(t => t.Holder is null).Select(t => t.Thread));
    var finished = new List<string>();
    while (ready.TryDequeue(out var done))
    {
        finished.Add(done);
        foreach (var t in threads.Where(t => t.Holder == done))
        {
            ready.Enqueue(t.Thread);
        }
    }
    var stuck = threads.Select(t => t.Thread).Except(finished).ToList();
    var canFinish = finished.Count == 0 ? "none" : string.Join(", ", finished);
    Console.WriteLine($"  can finish: {canFinish}");
    if (stuck.Count == 0)
    {
        Console.WriteLine("  deadlocked: none");
        return;
    }
    // Every stuck thread waits for another stuck thread, so following them must loop.
    var waits = threads.ToDictionary(t => t.Thread, t => t.Holder ?? t.Thread);
    var path = new List<string> { stuck[0] };
    while (!path.Contains(waits[path[^1]]))
    {
        path.Add(waits[path[^1]]);
    }
    var loop = path.Skip(path.IndexOf(waits[path[^1]])).ToList();
    loop.Add(loop[0]);
    var cycle = string.Join(" -> ", loop);
    Console.WriteLine($"  deadlocked: {string.Join(", ", stuck)} (cycle {cycle})");
}

In C#: The snapshot is an array of named tuples, Except keeps the stuck threads in the snapshot's order, and the dictionary maps a thread that waits for nobody to itself.

Recorded with C# 14 on .NET 10.0.12 (SDK 10.0.401) on macOS 26 arm64. It runs in your browser with the .NET runtime for WebAssembly 10.0.12. To run it yourself: dotnet run app.cs

C23 · main.c

#include <stddef.h>
#include <stdio.h>
#include <string.h>

// Each thread and the thread it waits for (a space: nobody).
typedef struct {
  const char *title;
  const char *threads, *holders;
} snapshot;

static void print_names(const char *names, size_t n, const char *sep) {
  for (size_t i = 0; i < n; i++)
    printf("%s%c", i > 0 ? sep : "", names[i]);
}

static void analyse(const char *threads, const char *holders) {
  const size_t n = strlen(threads);
  char ready[8] = "", finished[8] = "", stuck[8] = "", path[9] = "";
  size_t head = 0, tail = 0, n_finished = 0, n_stuck = 0, n_path = 0;
  // A thread that waits for nobody can finish and release its locks.
  for (size_t i = 0; i < n; i++)
    if (holders[i] == ' ')
      ready[tail++] = threads[i];
  while (head < tail) {
    const char done = ready[head++];
    finished[n_finished++] = done;
    for (size_t i = 0; i < n; i++)
      if (holders[i] == done)
        ready[tail++] = threads[i];
  }
  for (size_t i = 0; i < n; i++)
    if (memchr(finished, threads[i], n_finished) == NULL)
      stuck[n_stuck++] = threads[i];
  printf("  can finish: %s", n_finished == 0 ? "none" : "");
  print_names(finished, n_finished, ", ");
  if (n_stuck == 0) {
    printf("\n  deadlocked: none\n");
    return;
  }
  // Every stuck thread waits for another stuck thread, so following them must
  // loop.
  path[n_path++] = stuck[0];
  char next = holders[strchr(threads, stuck[0]) - threads];
  while (memchr(path, next, n_path) == NULL) {
    path[n_path++] = next;
    next = holders[strchr(threads, next) - threads];
  }
  const size_t start = (size_t)((char *)memchr(path, next, n_path) - path);
  path[n_path++] = next;
  printf("\n  deadlocked: ");
  print_names(stuck, n_stuck, ", ");
  printf(" (cycle ");
  print_names(path + start, n_path - start, " -> ");
  printf(")\n");
}

int main(void) {
  static const snapshot snapshots[] = {{"opposite orders", "AB", "BA"},
                                       {"same order", "AB", " A"},
                                       {"server", "ABCDE", "BCAE "},
                                       {"server and F", "ABCDEF", "BCAE A"}};
  for (size_t i = 0; i < sizeof snapshots / sizeof snapshots[0]; i++) {
    printf("%s\n", snapshots[i].title);
    analyse(snapshots[i].threads, snapshots[i].holders);
  }
  return 0;
}

In C: Each snapshot is two strings, the threads and, at the same position, the thread each one waits for, with a space for nobody; memchr and strchr do the searching.

Recorded with Apple clang 21.0.0 (C23) and GCC 16.2.0 on macOS 26 arm64. It runs in your browser with Clang in WebAssembly (YoWASP) 22.0.0-git20542-10. To run it yourself: clang -std=c23 -O2 -Wall -Wextra -Werror -pedantic -fsanitize=address,undefined main.c -o main && ./main

PHP 8.5 · main.php

<?php

declare(strict_types=1);

/**
 * Each thread and the thread it waits for (null: nobody).
 *
 * @param array<string, ?string> $waits
 */
function analyse(array $waits): void
{
    // A thread that waits for nobody can finish and release its locks.
    $ready = array_keys($waits, null, true);
    $finished = [];
    while ($ready !== []) {
        $done = array_shift($ready);
        $finished[] = $done;
        array_push($ready, ...array_keys($waits, $done, true));
    }
    $stuck = array_values(array_diff(array_keys($waits), $finished));
    echo '  can finish: ', $finished === [] ? 'none' : implode(', ', $finished), "\n";
    if ($stuck === []) {
        echo "  deadlocked: none\n";
        return;
    }
    // Every stuck thread waits for another stuck thread, so following them must loop.
    $path = [$stuck[0]];
    while (!in_array($waits[end($path)], $path, true)) {
        $path[] = $waits[end($path)];
    }
    $loop = array_slice($path, (int) array_search($waits[end($path)], $path, true));
    $loop[] = $loop[0];
    echo '  deadlocked: ', implode(', ', $stuck), ' (cycle ', implode(' -> ', $loop), ")\n";
}

$server = ['A' => 'B', 'B' => 'C', 'C' => 'A', 'D' => 'E', 'E' => null];
$snapshots = [
    'opposite orders' => ['A' => 'B', 'B' => 'A'],
    'same order' => ['A' => null, 'B' => 'A'],
    'server' => $server,
    'server and F' => $server + ['F' => 'A'],
];
foreach ($snapshots as $title => $waits) {
    echo $title, "\n";
    analyse($waits);
}

In PHP: array_keys($waits, null, true) finds the threads whose holder is exactly null, and the + operator appends F to a copy of the server snapshot.

Recorded with PHP 8.5.11 (CLI) on macOS 26 arm64. It runs in your browser with php-wasm 0.2.0 (PHP 8.5.2). To run it yourself: php main.php

Rust 1.99 · main.rs

/// Each thread and the thread it waits for (None: nobody).
type Threads<'a> = [(&'a str, Option<&'a str>)];

fn analyse(threads: &Threads) {
    // A thread that waits for nobody can finish and release its locks.
    let mut ready: Vec<&str> = threads
        .iter()
        .filter(|(_, h)| h.is_none())
        .map(|(t, _)| *t)
        .collect();
    let mut finished = Vec::new();
    while !ready.is_empty() {
        let done = ready.remove(0);
        finished.push(done);
        ready.extend(
            threads
                .iter()
                .filter(|(_, h)| *h == Some(done))
                .map(|(t, _)| *t),
        );
    }
    let stuck: Vec<&str> = threads
        .iter()
        .map(|(t, _)| *t)
        .filter(|t| !finished.contains(t))
        .collect();
    let can = if finished.is_empty() {
        "none".to_string()
    } else {
        finished.join(", ")
    };
    println!("  can finish: {can}");
    let Some(&first) = stuck.first() else {
        println!("  deadlocked: none");
        return;
    };
    // Every stuck thread waits for another stuck thread, so following them must loop.
    let holder = |t: &str| {
        threads
            .iter()
            .find(|(name, _)| *name == t)
            .and_then(|(_, h)| *h)
    };
    let mut path = vec![first];
    let mut next = holder(first).unwrap_or(first);
    while !path.contains(&next) {
        path.push(next);
        next = holder(next).unwrap_or(next);
    }
    let mut cycle = path.split_off(path.iter().position(|&t| t == next).unwrap_or(0));
    cycle.push(next);
    println!(
        "  deadlocked: {} (cycle {})",
        stuck.join(", "),
        cycle.join(" -> ")
    );
}

fn main() {
    let server = [
        ("A", Some("B")),
        ("B", Some("C")),
        ("C", Some("A")),
        ("D", Some("E")),
        ("E", None),
    ];
    let mut with_f = server.to_vec();
    with_f.push(("F", Some("A")));
    let snapshots = [
        ("opposite orders", vec![("A", Some("B")), ("B", Some("A"))]),
        ("same order", vec![("A", None), ("B", Some("A"))]),
        ("server", server.to_vec()),
        ("server and F", with_f),
    ];
    for (title, threads) in &snapshots {
        println!("{title}");
        analyse(threads);
    }
}

In Rust: Option<&str> marks a thread that waits for nobody, and split_off cuts the path at the thread where the cycle starts, so only the cycle is left in the second half.

Recorded with rustc 1.99.0 (edition 2024) on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: rustc --edition 2024 -O main.rs && ./main

TypeScript 6.0 · main.ts

// Each thread and the thread it waits for (null: nobody).
type Threads = Record<string, string | null>;

const SERVER: Threads = { A: "B", B: "C", C: "A", D: "E", E: null };
const SNAPSHOTS: Record<string, Threads> = {
  "opposite orders": { A: "B", B: "A" },
  "same order": { A: null, B: "A" },
  server: SERVER,
  "server and F": { ...SERVER, F: "A" },
};

function analyse(waits: Map<string, string | null>): void {
  // A thread that waits for nobody can finish and release its locks.
  const ready = [...waits.keys()].filter((t) => waits.get(t) === null);
  const finished: string[] = [];
  for (let done = ready.shift(); done !== undefined; done = ready.shift()) {
    finished.push(done);
    ready.push(...[...waits.keys()].filter((t) => waits.get(t) === done));
  }
  const stuck = [...waits.keys()].filter((t) => !finished.includes(t));
  console.log(`  can finish: ${finished.join(", ") || "none"}`);
  if (stuck.length === 0) {
    console.log("  deadlocked: none");
    return;
  }
  // Every stuck thread waits for another stuck thread, so following them must loop.
  const path = [stuck[0]];
  const next = (): string => waits.get(path[path.length - 1]) ?? "";
  while (!path.includes(next())) path.push(next());
  const loop = path.slice(path.indexOf(next()));
  console.log(`  deadlocked: ${stuck.join(", ")} (cycle ${[...loop, loop[0]].join(" -> ")})`);
}

for (const [title, threads] of Object.entries(SNAPSHOTS)) {
  console.log(title);
  analyse(new Map(Object.entries(threads)));
}

In TypeScript: A Record type describes each snapshot, and next() falls back to an empty string so that path stays a string[] under strict mode.

Recorded with TypeScript 6.0.3 (strict), run by Node.js 24.21.0 on macOS 26 arm64. It runs in your browser with QuickJS (quickjs-emscripten 0.32.0) after Sucrase 3.35.1. To run it yourself: npx -p typescript@6.0.3 tsc --noEmit --strict main.ts && node main.ts

Go 1.27 · main.go

package main

import (
	"fmt"
	"slices"
	"strings"
)

// Each thread and the thread it waits for ("": nobody).
type waiting struct{ thread, holder string }

func analyse(threads []waiting) {
	// A thread that waits for nobody can finish and release its locks.
	var ready, finished, stuck []string
	for _, w := range threads {
		if w.holder == "" {
			ready = append(ready, w.thread)
		}
	}
	for len(ready) > 0 {
		done := ready[0]
		ready = ready[1:]
		finished = append(finished, done)
		for _, w := range threads {
			if w.holder == done {
				ready = append(ready, w.thread)
			}
		}
	}
	holders := map[string]string{}
	for _, w := range threads {
		holders[w.thread] = w.holder
		if !slices.Contains(finished, w.thread) {
			stuck = append(stuck, w.thread)
		}
	}
	if len(finished) == 0 {
		finished = []string{"none"}
	}
	fmt.Println("  can finish:", strings.Join(finished, ", "))
	if len(stuck) == 0 {
		fmt.Println("  deadlocked: none")
		return
	}
	// Every stuck thread waits for another stuck thread, so following them must loop.
	path := []string{stuck[0]}
	for !slices.Contains(path, holders[path[len(path)-1]]) {
		path = append(path, holders[path[len(path)-1]])
	}
	loop := slices.Clone(path[slices.Index(path, holders[path[len(path)-1]]):])
	loop = append(loop, loop[0])
	fmt.Printf("  deadlocked: %s (cycle %s)\n",
		strings.Join(stuck, ", "), strings.Join(loop, " -> "))
}

func main() {
	server := []waiting{{"A", "B"}, {"B", "C"}, {"C", "A"}, {"D", "E"}, {"E", ""}}
	snapshots := []struct {
		title   string
		threads []waiting
	}{
		{"opposite orders", []waiting{{"A", "B"}, {"B", "A"}}},
		{"same order", []waiting{{"A", ""}, {"B", "A"}}},
		{"server", server},
		{"server and F", append(slices.Clone(server), waiting{"F", "A"})},
	}
	for _, s := range snapshots {
		fmt.Println(s.title)
		analyse(s.threads)
	}
}

In Go: Go's maps have no order, so each snapshot is a slice of structs; an empty string marks a thread that waits for nobody.

Recorded with Go 1.27.1 on macOS 26 arm64. It runs in your browser with the Go compiler in WebAssembly 1.27.1. To run it yourself: go run main.go

Swift 6.4 · main.swift

// Each thread and the thread it waits for (nil: nobody).
typealias Threads = [(thread: String, holder: String?)]

func analyse(_ threads: Threads) {
  // A thread that waits for nobody can finish and release its locks.
  var ready = threads.filter { $0.holder == nil }.map(\.thread)
  var finished: [String] = []
  while !ready.isEmpty {
    let done = ready.removeFirst()
    finished.append(done)
    ready += threads.filter { $0.holder == done }.map(\.thread)
  }
  let stuck = threads.map(\.thread).filter { !finished.contains($0) }
  print("  can finish: \(finished.isEmpty ? "none" : finished.joined(separator: ", "))")
  guard let first = stuck.first else {
    print("  deadlocked: none")
    return
  }
  // Every stuck thread waits for another stuck thread, so following them must loop.
  func holder(of thread: String) -> String {
    threads.first { $0.thread == thread }?.holder ?? thread
  }
  var path = [first]
  while !path.contains(holder(of: path[path.count - 1])) {
    path.append(holder(of: path[path.count - 1]))
  }
  let start = holder(of: path[path.count - 1])
  let loop = path[(path.firstIndex(of: start) ?? 0)...] + [start]
  print("  deadlocked: \(stuck.joined(separator: ", ")) (cycle \(loop.joined(separator: " -> ")))")
}

let server: Threads = [("A", "B"), ("B", "C"), ("C", "A"), ("D", "E"), ("E", nil)]
let snapshots: [(title: String, threads: Threads)] = [
  ("opposite orders", [("A", "B"), ("B", "A")]),
  ("same order", [("A", nil), ("B", "A")]),
  ("server", server),
  ("server and F", server + [("F", "A")]),
]
for snapshot in snapshots {
  print(snapshot.title)
  analyse(snapshot.threads)
}

In Swift: A snapshot is an array of labelled tuples, and holder(of:) is a nested function that searches it, falling back to the thread itself instead of force-unwrapping.

Recorded with Apple Swift 6.4 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: swiftc -O main.swift -o main && ./main

Ruby 4.0 · main.rb

# frozen_string_literal: true

# A thread that waits for nobody can finish and release its locks.
def finish_order(waits)
  ready = waits.select { |_, holder| holder.nil? }.keys
  finished = []
  until ready.empty?
    done = ready.shift
    finished << done
    ready.concat(waits.select { |_, holder| holder == done }.keys)
  end
  finished
end

# Every stuck thread waits for another stuck thread, so following them must loop.
def cycle_from(first, waits)
  path = [first]
  path << waits[path.last] until path.include?(waits[path.last])
  loop = path.drop(path.index(waits[path.last]))
  loop << loop.first
end

def analyse(waits)
  finished = finish_order(waits)
  stuck = waits.keys - finished
  puts "  can finish: #{finished.empty? ? 'none' : finished.join(', ')}"
  return puts('  deadlocked: none') if stuck.empty?

  puts "  deadlocked: #{stuck.join(', ')} (cycle #{cycle_from(stuck.first, waits).join(' -> ')})"
end

# Each thread and the thread it waits for (nil: nobody).
server = { 'A' => 'B', 'B' => 'C', 'C' => 'A', 'D' => 'E', 'E' => nil }
{
  'opposite orders' => { 'A' => 'B', 'B' => 'A' },
  'same order' => { 'A' => nil, 'B' => 'A' },
  'server' => server,
  'server and F' => server.merge('F' => 'A')
}.each do |title, waits|
  puts title
  analyse(waits)
end

In Ruby: RuboCop keeps a method to ten lines, so the finishing and the cycle are separate methods; waits.keys - finished keeps the snapshot's order.

Recorded with CRuby 4.0.7 on macOS 26 arm64. It runs in your browser with ruby.wasm 2.10.1 (Ruby 4.0.0). To run it yourself: ruby main.rb

Kotlin 2.4 · Main.kt

fun analyse(waits: Map<String, String?>) {
    // A thread that waits for nobody can finish and release its locks.
    val ready = ArrayDeque(waits.filterValues { it == null }.keys)
    val finished = mutableListOf<String>()
    while (ready.isNotEmpty()) {
        val done = ready.removeFirst()
        finished += done
        ready.addAll(waits.filterValues { it == done }.keys)
    }
    val stuck = waits.keys - finished.toSet()
    println("  can finish: ${if (finished.isEmpty()) "none" else finished.joinToString(", ")}")
    if (stuck.isEmpty()) {
        println("  deadlocked: none")
        return
    }
    // Every stuck thread waits for another stuck thread, so following them must loop.
    val path = mutableListOf(stuck.first())
    var next = waits.getValue(path.last()) ?: path.last()
    while (next !in path) {
        path += next
        next = waits.getValue(next) ?: next
    }
    val loop = path.drop(path.indexOf(next))
    val cycle = (loop + loop.first()).joinToString(" -> ")
    println("  deadlocked: ${stuck.joinToString(", ")} (cycle $cycle)")
}

fun main() {
    // Each thread and the thread it waits for (null: nobody).
    val server = linkedMapOf("A" to "B", "B" to "C", "C" to "A", "D" to "E", "E" to null)
    val snapshots =
        listOf(
            "opposite orders" to mapOf("A" to "B", "B" to "A"),
            "same order" to mapOf("A" to null, "B" to "A"),
            "server" to server,
            "server and F" to server + ("F" to "A"),
        )
    for ((title, waits) in snapshots) {
        println(title)
        analyse(waits)
    }
}

In Kotlin: filterValues keeps the map's insertion order, and getValue(next) ?: next keeps the walk free of null without an assertion, since a stuck thread always waits for another one.

Recorded with Kotlin 2.4.20 on Java 25.0.4.1 on macOS 26 arm64. It runs in your browser with a WebAssembly build on the WASI runner (browser_wasi_shim 0.4.2). To run it yourself: kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar

Dart 3.13 · main.dart

// Each thread and the thread it waits for (null: nobody).
void analyse(Map<String, String?> waits) {
  // A thread that waits for nobody can finish and release its locks.
  final ready = [
    for (final MapEntry(:key, :value) in waits.entries)
      if (value == null) key,
  ];
  final finished = <String>[];
  while (ready.isNotEmpty) {
    final done = ready.removeAt(0);
    finished.add(done);
    ready.addAll([
      for (final MapEntry(:key, :value) in waits.entries)
        if (value == done) key,
    ]);
  }
  final stuck = [
    for (final t in waits.keys)
      if (!finished.contains(t)) t,
  ];
  print('  can finish: ${finished.isEmpty ? 'none' : finished.join(', ')}');
  if (stuck.isEmpty) {
    print('  deadlocked: none');
    return;
  }
  // Every stuck thread waits for another stuck thread, so following them must loop.
  final path = [stuck.first];
  var next = waits[path.last] ?? path.last;
  while (!path.contains(next)) {
    path.add(next);
    next = waits[next] ?? next;
  }
  final loop = [...path.sublist(path.indexOf(next)), next];
  print('  deadlocked: ${stuck.join(', ')} (cycle ${loop.join(' -> ')})');
}

void main() {
  final server = {'A': 'B', 'B': 'C', 'C': 'A', 'D': 'E', 'E': null};
  final snapshots = {
    'opposite orders': {'A': 'B', 'B': 'A'},
    'same order': {'A': null, 'B': 'A'},
    'server': server,
    'server and F': {...server, 'F': 'A'},
  };
  snapshots.forEach((title, waits) {
    print(title);
    analyse(waits);
  });
}

In Dart: Map literals keep their insertion order, and MapEntry patterns in the collection-for pick each thread with its holder.

Recorded with Dart 3.13.5 on macOS 26 arm64. It runs in your browser with a WebAssembly build by dart2wasm 3.13.5. To run it yourself: dart run main.dart

What every language printed

opposite orders
  can finish: none
  deadlocked: A, B (cycle A -> B -> A)
same order
  can finish: A, B
  deadlocked: none
server
  can finish: E, D
  deadlocked: A, B, C (cycle A -> B -> C -> A)
server and F
  can finish: E, D
  deadlocked: A, B, C, F (cycle A -> B -> C -> A)

The server’s answer matches the figure: E and D finish, and A, B and C are deadlocked around A -> B -> C -> A. In the last snapshot F is not on the cycle, but it waits for the log lock that A holds, so F never finishes either. A thread that waits for a deadlocked thread is part of the deadlock, which is why the program lists every thread that cannot finish before it names the cycle at the heart of them.

Checkpoint

Check: Resource-allocation and wait-for graphs

  1. Question 1 of 2 In a resource-allocation graph, what does an arrow from resource R to thread T mean?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: T holds an instance of R

    Requests point from threads to resources and holdings from resources to threads, so a cycle alternates between the two: a thread wants a resource that a thread holds, which wants a resource, and so on.

  2. Question 2 of 2 In the server snapshot, which threads can finish, and in what order?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: E, then D

    The figure and the program agree: E finishes, D follows, and A, B and C stay on the cycle A -> B -> C -> A.

With several instances, a cycle is not yet a deadlock

So far every lock had one instance. Then a cycle is a deadlock: each resource on the cycle is held by a thread on the cycle, and that thread is waiting. A pool of identical resources, such as database connections, worker threads or buffers, breaks that argument. A thread that wants a connection can take any free one, including one that a thread outside the cycle gives back. Step through such a snapshot, a pool of two connections and one file lock:

Interactive · step by step A cycle through a pool of two connections Step through the snapshot. The pool has two connections and the file lock one; watch who finishes, and in what order.
A cycle through a pool of two connections, step 6 of 6. Showing Thread A (finished), Report file lock (held by A), Connection pool (2 connections, 1 free), Thread B (finished), Thread C (finished).held byheld byThread AfinishedReport file lockheld by AConnection pool2 connections, 1 freeThread BfinishedThread Cfinished

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. A cycle through the pool. A holds one of the pool's two connections and wants the file lock. B holds the file lock and wants a connection. The arrows A to file lock to B to pool and back to A form a cycle.
  2. C holds the other connection. The pool's second connection belongs to C, which waits for nothing because it is running a query. A thread outside the cycle holds an instance that the cycle needs, so the cycle does not settle the question yet.
  3. C finishes. C finishes its query and returns its connection, so the pool has one free. C waited for nobody, so it was always going to finish.
  4. B gets a connection. B's request can now be met. B holds the file lock and a connection, waits for nothing and runs to its end.
  5. B finishes. B releases the connection and the file lock. The file lock is free, and so is one connection.
  6. A takes the file lock. A takes the file lock and finishes as well. Every thread finished, so this snapshot was never a deadlock, although its graph held a cycle.

The cycle was real, and still nothing was stuck: C held the instance that the cycle needed, and C waited for nobody. Holt’s analysis gives the rule. With one instance per resource, a cycle in the graph is a deadlock. With several instances, a deadlock still needs a cycle, but a cycle is not enough to prove one. To decide, finish whatever can finish, counting the free instances of each resource as threads return them, and see who is left. The last lesson of this module turns that procedure into the detection algorithm, with counts in tables instead of arrows.

Checkpoint

Check: Cycles when a resource has several instances

  1. Question 1 of 2 In the pool snapshot, suppose C also wants the report file lock that B holds, instead of running freely. A and C hold the two connections, A wants the file lock, and B wants a connection. What is the verdict?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: All three are deadlocked

    With several instances a cycle alone does not settle it, so finish what can finish: here nothing can, because every thread is waiting, and the whole group is deadlocked.

  2. Question 2 of 2 Every resource in a snapshot has exactly one instance, and its resource-allocation graph has a cycle. What does that prove?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The threads on the cycle are deadlocked

    With one instance per resource, a cycle is both necessary and enough for deadlock; with several instances it is only necessary.

Livelock and starvation stop progress without a cycle

Threads can fail to make progress without being deadlocked. In a livelock: Threads that keep running and reacting to each other, for example by releasing and retrying in step, while none of them makes progress. they keep running and keep reacting to one another, a case Oracle’s Java tutorial describes beside deadlock, and none of them gets anywhere. In starvation one thread waits while the others keep getting the resource: a lock that always goes to the thread that asks again first, a stream of readers that never leaves a gap for a writer, or a thread whose priority is always lower than someone else’s.

Suppose the two transfers are rewritten to avoid the deadlock politely: a thread that cannot get its second lock at once releases its first and tries again. Step through what happens when both threads keep the same pace:

Interactive · message by message Two polite threads that retry in step Step through two rounds of the same transfers, now written with a try-lock that gives up at once and retries.
Two polite threads that retry in step, step 7 of 7. Messages so far: Thread A to Locks 17 and 42: lock 17; Thread B to Locks 17 and 42: lock 42; Thread A to Locks 17 and 42: try 42, busy; Thread B to Locks 17 and 42: try 17, busy; Thread A to Locks 17 and 42: release 17; Thread B to Locks 17 and 42: lock 42 again; Thread A to Locks 17 and 42: lock 17 again.Thread Amoves 17 to 42Locks 17 and 42Thread Bmoves 42 to 17lock 17lock 42try 42, busytry 17, busyrelease 17lock 42 againlock 17 again

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. A takes lock 17. Each thread now asks for its second lock with a try-lock, which answers at once instead of waiting. A starts by locking account 17.
  2. B takes lock 42. At the same moment B locks account 42, exactly as in the deadlocked run.
  3. A tries 42. A tries lock 42 and hears that it is taken. Instead of waiting with lock 17 in hand, A is polite and lets go of 17 to try again.
  4. B tries 17. B tries lock 17 at the same moment, hears that it is taken, and decides the same thing.
  5. Both let go. A releases lock 17 and B releases lock 42. Nobody holds anything and nobody is blocked.
  6. Both start again. B has released 42 too, and after the same short pause both threads start over, A with lock 17 and B with lock 42.
  7. The same moves for ever. The pair is back where it began. With the same timing the round repeats without end. That is a livelock, both threads running and changing state while no transfer ever finishes.

Neither thread was ever blocked for long, and still no transfer finished. Real timing often breaks such a pattern after a few rounds, but nothing guarantees it, and the usual cure is to make the pauses unequal, for example random. The three failures look alike from outside, a request that never completes, and differ inside:

What differs Deadlock Livelock Starvation
The threads are blocked keep running one waits, others run
Progress none in the group none in the group every thread but the starved one
A cycle in the wait-for graph always none that lasts none
What ends it breaking one Coffman condition breaking the symmetry a fair order, such as a queue

Checkpoint

Check: Deadlock, livelock and starvation

  1. Question 1 of 2 Two worker threads use 100 % of their cores for ten minutes, the logs show them taking and releasing the same two locks thousands of times, and no job completes. Which failure is this most likely to be?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Livelock

    Busy threads without progress point to livelock, which random or unequal back-off usually ends.

  2. Question 2 of 2 A cache uses a readers-writer lock. Readers arrive so often that some reader always holds it, and the thread that wants to write a new value waits for minutes while thousands of reads complete. What is this?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Starvation of the writer

    Starvation is a fairness problem: give the lock a queue, or let a waiting writer stop new readers from entering.

Deadlocks hide in lock order, callbacks and full pools

In real programs the two locks are rarely taken side by side in one function, where anyone reading the code would see them. Three patterns hide the cycle well:

Where it hides How the cycle forms
Lock order across modules Module X holds its own lock while it calls module Y, which takes Y’s lock; another path holds Y’s lock and calls back into X
A callback under a lock A method holds its object’s lock while it calls a method of another object that locks that object; two threads doing it in opposite directions deadlock, like the two bowing friends of Oracle’s Java tutorial
A full thread pool: A fixed set of threads, started once, that take tasks from a shared queue and run them one after another, so that tasks need no thread of their own. More in “Thread pools, event loops and async I/O” Every worker of a fixed pool runs a task that waits for another task still in the queue, which needs a free worker; Python’s documentation shows it with a pool of one worker

None of these is visible in a single function. That is why tools record the order in which locks are taken across a whole program: the Linux kernel’s lockdep validator reports two lock classes taken in both orders, even when no run has deadlocked yet. The pool case holds no lock at all. Its resource is the pool’s workers, and its cycle runs through the task queue: each running task waits for a queued task, and every queued task waits for a worker.

Checkpoint

Check: Where deadlocks hide in real code

  1. Question 1 of 2 A pool has one worker. Its task submits a second task to the same pool and waits for the result. No lock is used anywhere. What happens?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It deadlocks, because the only worker waits for a task that needs a worker

    Python's documentation of ThreadPoolExecutor shows this exact case with one worker. The cycle runs through the queue, from the waiting task to the queued one and back to the worker.

  2. Question 2 of 2 A method holds its object's lock while it calls listener callbacks, and one listener calls back into another object that takes its own lock. Two threads sometimes deadlock. Which change removes the cycle?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Copy the list of listeners under the lock, release the lock, then call them

    Calling code you do not control while holding a lock is how lock orders get tangled across modules; calling it with no lock held keeps your lock out of every chain.

Practise: name the deadlocked threads

Write the reduction yourself. The function receives a snapshot like the program’s, each thread with the locks it holds and the lock it wants, and returns the threads that can never finish. The sample tests run in your browser and include a thread that waits behind a cycle, two separate cycles and a thread that waits for a lock it already holds:

Exercise · Medium · Python

Name the threads that can never finish

A snapshot lists every thread as a tuple (name, held, wants): the thread's name, the list of locks it holds, and the lock it waits for, or None when it waits for nothing. Every lock has one instance, and a thread that wants a lock nobody holds simply takes it.

Write deadlocked(threads), which returns the sorted list of the names of the threads that can never finish. A thread that waits for nobody can finish and release its locks; a thread can finish once every lock it waits for has been released. A thread that waits for a deadlocked thread never finishes either, and an empty list means that every thread can finish.

Starter code · deadlocked.py

def deadlocked(threads):
    """Returns the sorted names of the threads that can never finish.

    threads is a list of (name, held, wants): the thread's name, the locks it holds and
    the lock it waits for, or None. Every lock has one instance.
    """
    # Replace this line with your code.
    return []
The sample tests · test_deadlocked.py
from deadlocked import deadlocked

SERVER = [
    ("A", ["log"], "cache"),
    ("B", ["cache"], "index"),
    ("C", ["index"], "log"),
    ("D", [], "stats"),
    ("E", ["stats"], None),
]


def test_opposite_orders():
    """two threads that each hold the lock the other wants"""
    assert deadlocked([("A", ["17"], "42"), ("B", ["42"], "17")]) == ["A", "B"]


def test_same_order():
    """a thread that waits behind a running thread is not deadlocked"""
    assert deadlocked([("A", ["17", "42"], None), ("B", [], "17")]) == []


def test_server():
    """a cycle of three while two other threads finish"""
    assert deadlocked(SERVER) == ["A", "B", "C"]


def test_thread_behind_the_cycle():
    """a thread that waits for a deadlocked thread never finishes either"""
    assert deadlocked(SERVER + [("F", [], "log")]) == ["A", "B", "C", "F"]


def test_two_separate_cycles():
    """two cycles at once, beside a thread that waits for nothing"""
    threads = [
        ("A", ["1"], "2"),
        ("B", ["2"], "1"),
        ("C", ["3"], "4"),
        ("D", ["4"], "3"),
        ("E", [], None),
    ]
    assert deadlocked(threads) == ["A", "B", "C", "D"]


def test_lock_nobody_holds():
    """a thread that wants a lock nobody holds takes it, and the chain behind it finishes"""
    assert deadlocked([("A", ["1"], "2"), ("B", [], "1")]) == []


def test_waiting_for_itself():
    """a thread that waits for a lock it already holds deadlocks on its own"""
    assert deadlocked([("A", ["1"], "1"), ("B", [], None)]) == ["A"]
A hint

Start from the threads that wait for nobody: they want None, or a lock that no thread holds. Each one that finishes lets the threads that were waiting for it go next. The threads you never reach are the deadlocked ones.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Then test the whole lesson. The questions open on whichever of its six ideas you have answered worst, and each right answer brings a harder one:

Practice that adapts to you

Practise this lesson

16 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 16 Which description fits a deadlock?

    Choose one answer.

    Show the answer: Question 1 of 16

    Answer: Every thread of a group waits for something that only another thread of the same group can do

    What makes it permanent is that the thing each thread waits for depends on another waiting thread.

  2. Question 2 of 16 If two_locks.c used pthread_mutex_lock instead of a one-second try loop, how would its first run end?

    Choose one answer.

    Show the answer: Question 2 of 16

    Answer: It would never end; both threads would wait for ever

    The deadline in two_locks.c only turns the deadlock into a failure you can see; it does not prevent it.

  3. Question 3 of 16 Which of these are Coffman conditions for deadlock?

    Choose every answer that is right.

    Show the answer: Question 3 of 16

    Answer:

    • No preemption
    • Mutual exclusion
    • Hold and wait
    • Circular wait

    The four together are necessary for every deadlock; remove any one and the waiting can always end.

  4. Question 4 of 16 Two threads only ever take a readers-writer lock in shared mode, for reading. Can they deadlock each other on that lock alone?

    Choose one answer.

    Show the answer: Question 4 of 16

    Answer: No, because shared mode lets both hold the lock at once, so neither waits for the other

    Removing mutual exclusion is the first way to prevent deadlock, and shared read access is the everyday case of it. A writer that arrives changes the picture, because the writer and the readers do exclude each other.

  5. Question 5 of 16 A database breaks a deadlock by aborting one waiting transaction and taking its locks away. Which Coffman condition has it removed, and what does that cost?

    Choose one answer.

    Show the answer: Question 5 of 16

    Answer: No preemption; the aborted transaction loses its work and must start again

    Preemption works only for resources whose holder can be rolled back, which is why databases use it and ordinary programs holding mutexes cannot.

  6. Question 6 of 16 A thread T waits for a lock L. Which arrow does a resource-allocation graph draw for that?

    Choose one answer.

    Show the answer: Question 6 of 16

    Answer: An arrow from T to L

    In the wait-for graph the same request becomes an arrow from T to the thread that holds L.

  7. Question 7 of 16 A holds lock 1 and wants lock 2. B holds lock 2 and wants lock 1. C holds lock 3 and wants lock 1. D holds nothing and wants lock 3. E holds lock 4 and wants nothing. How many threads are deadlocked?

    Type a number.

    A hint

    Finish every thread that waits for nobody, then every thread that waited only for a finished one.

    Show the answer: Question 7 of 16

    Answer: 4 threads

    Only E waits for nobody, and nobody waits for E. A and B wait for each other, C waits for A, and D waits for C, so A, B, C and D can never finish: 4 threads. Only A and B are on the cycle.

  8. Question 8 of 16 Why may the resources be left out of the graph, leaving a wait-for graph, only when each resource has one instance?

    Choose one answer.

    Show the answer: Question 8 of 16

    Answer: With one instance, a request points through the resource at exactly one holder, so "waits for" names one thread

    The wait-for graph is a shortcut for single-instance resources; pools need the counts that the full graph keeps.

  9. Question 9 of 16 Some resources have several instances, and the resource-allocation graph has a cycle. What follows?

    Choose one answer.

    Show the answer: Question 9 of 16

    Answer: A deadlock is possible, and finishing what can finish decides it

    With pools, a cycle is necessary for deadlock but not enough; the counts decide.

  10. Question 10 of 16 A pool has 3 connections, held by A, B and C. A and B each also want the file lock, which D holds, and D wants a connection. C waits for nothing. Who is deadlocked?

    Choose one answer.

    Show the answer: Question 10 of 16

    Answer: Nobody, because C finishes, D takes its connection and finishes, and then A and B can go on

    The cycle A -> file lock -> D -> pool -> A exists, but C, outside it, holds an instance of the pool and finishes.

  11. Question 11 of 16 Can threads using pooled resources be deadlocked when the resource-allocation graph has no cycle at all?

    Choose one answer.

    Show the answer: Question 11 of 16

    Answer: No; with or without pools, every deadlock shows a cycle in the graph

    With several instances a cycle is necessary but not sufficient; it is never absent from a deadlock.

  12. Question 12 of 16 Which statement is true of livelocked threads?

    Choose one answer.

    Show the answer: Question 12 of 16

    Answer: They keep running and reacting to each other, but none of them makes progress

    The polite transfers of the lockstep scene took and released their locks again and again without finishing.

  13. Question 13 of 16 A low-priority job never runs on a busy machine although the other jobs keep finishing. Which failure is it?

    Choose one answer.

    Show the answer: Question 13 of 16

    Answer: Starvation

    Schedulers fix this with aging, raising a waiting job's priority over time; locks fix it with a fair order.

  14. Question 14 of 16 The two polite transfers retried in step for ever. Why do random pauses before each retry end the livelock?

    Choose one answer.

    Show the answer: Question 14 of 16

    Answer: Sooner or later one thread retries while the other is still pausing, gets both locks and finishes

    Livelock is a symmetry problem: threads that react identically to each other. Breaking the symmetry, with randomness or an order, lets one of them through.

  15. Question 15 of 16 Each request in a pool of 4 workers waits for two sub-tasks submitted to the same pool. Which change removes the deadlock?

    Choose one answer.

    Show the answer: Question 15 of 16

    Answer: Run the sub-tasks on a separate pool, so a waiting request never blocks the workers its sub-tasks need

    The other cure is not to wait inside a pool task at all: chain the work so that the last sub-task finishes the request.

  16. Question 16 of 16 The kernel's lockdep validator reports that lock classes X and Y were taken in both orders, although no test has ever hung. Why is the report worth acting on?

    Choose one answer.

    Show the answer: Question 16 of 16

    Answer: Two paths that take X and Y in opposite orders can interleave into a cycle, even if no run has done so yet

    Recording lock order turns a rare timing bug into a deterministic report: fix it by choosing one order for X and Y.

Interview questions

The warm-up comes with its full answer:

  1. Fresher to mid · Warm-up

    What four conditions hold whenever there is a deadlock, and what does each of them mean?

    Mutual exclusion: a resource can be used by one thread at a time, so the others must wait for it. Hold and wait: a thread keeps what it already holds while it waits for more. No preemption: nothing takes a resource away from the thread that holds it; it is released only when that thread decides to. Circular wait: there is a closed chain of threads in which each waits for a resource held by the next. Coffman, Elphick and Shoshani described the four in 1971, and every deadlock satisfies all of them at once, so breaking any one prevents deadlock. The first three describe how resources are handed out and hold for ordinary locks; circular wait describes the state the threads reach, which is why a global lock order, which makes a cycle impossible, is the usual cure.

Two harder questions start from this lesson: a service that hangs about once a week, and a thread pool that stops with no lock held. Pick a first move for each; every answer explains what it catches and what it misses:

Checkpoint

Your first decision

  1. Question 1 of 2 A service hangs about once a week at peak load, and a thread dump taken during a hang shows 40 blocked threads. What is your first move?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: For each blocked thread, write down what it waits for and who holds it, then look for a cycle

    A deadlock is a cycle among waiting threads, so the snapshot has to say who waits for whom. A second dump a minute later shows whether the same threads stay stuck.

  2. Question 2 of 2 A fixed pool of 8 workers runs requests, and each request waits for two sub-tasks that it submits to the same pool. Under load everything stops, with no lock held. What do you suspect first?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: All 8 workers are waiting for sub-tasks that sit in the queue, waiting for a free worker

    Waiting inside a pool task for work queued in the same pool deadlocks as soon as every worker does it at once.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A service hangs about once a week at peak load, and a thread dump taken during a hang shows 40 blocked threads. How do you decide whether this is a deadlock rather than slow I/O or starvation, and how do you find the threads and locks involved?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A service runs requests on a fixed pool of 8 worker threads, and each request submits two sub-tasks to the same pool and waits for both. Under load the service stops completely, yet no thread holds a lock. Explain what happened and how you would fix it.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A deadlock is a group of threads in which each waits for something that only another member can do; two locks taken in opposite orders are enough to make one.
  • Four Coffman conditions hold in every deadlock, circular wait among them, and a design that rules out any one of the four lets every wait end.
  • A resource-allocation graph draws requests and holdings as arrows; with one instance per resource it shrinks to a wait-for graph, and finishing every thread that can finish leaves exactly the deadlocked threads.
  • With one instance per resource a cycle is a deadlock; with several instances a cycle is only a warning, settled by finishing what can finish and counting free instances.
  • Livelocked threads keep running without progress, and a starved thread waits while others progress; neither shows a lasting cycle.
  • Most deadlocks in real code come from lock order across modules, callbacks made under a lock and tasks that wait for other tasks of the same full pool.

Terms in this lesson

circular wait
A condition for deadlock: a closed chain of threads in which each one waits for a resource that the next one holds.
Coffman conditions
The four conditions that hold whenever a deadlock exists: mutual exclusion, hold and wait, no preemption and circular wait. Removing any one of them prevents deadlock.
deadlock
A state in which every thread of a group waits for something that only another thread of the same group can do, such as releasing a lock, so none of them can ever go on.
hold and wait
A condition for deadlock: a thread keeps the resources it already holds while it waits for another one.
livelock
Threads that keep running and reacting to each other, for example by releasing and retrying in step, while none of them makes progress.
resource-allocation graph
A graph of threads and resources in which an arrow from a thread to a resource is a request and an arrow from a resource to a thread means that the thread holds it.
wait-for graph
A graph with an arrow from each waiting thread to the thread that holds what it waits for. When every resource has one instance, a cycle in it is a deadlock.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.