Deadlock#

Deadlock occurs when two or more threads are each waiting for resources held by another thread in the same waiting cycle. Once the cycle forms, the threads cannot make progress unless something external breaks the cycle.

Deadlock may not happen immediately in a program that contains the conditions for it. The bug may depend on timing, scheduling, input, or the order in which threads reach the locks.

Dining Philosophers Problem#

The dining philosophers problem is a small model of resource contention. Each philosopher alternates between thinking and eating, and each needs two forks to eat.

The usual rules are:

  • A philosopher can be eating or thinking.

  • Philosophers do not communicate with each other.

  • A philosopher needs both the left fork and the right fork before eating.

  • Each fork can be held by only one philosopher at a time.

Dining Philosophers Deadlock Scenario#

A deadlock can occur if every philosopher picks up one fork and then waits for the other fork.

eat() {
   pick up right fork;
   pick up left fork;
   proceed with eating;
}

One possible execution is:

  • At t = 0, every philosopher picks up the fork on the right.

  • At t = 1, philosopher 1 tries to pick up the left fork, but philosopher 5 holds it.

  • At t = 2, philosopher 2 tries to pick up the left fork, but philosopher 1 holds it.

  • The same pattern continues around the table.

Every philosopher holds one fork and waits for a fork held by another philosopher. No philosopher can finish eating and release a fork, so the program is deadlocked.

Dining Philosophers Lock Graph#

A lock graph shows the possible ordering relationships among locks. If the graph contains a cycle, a deadlock is possible.

In the first dining philosophers algorithm, forks can be acquired in an order that forms a cycle.

Undirected Lock Graph

Avoiding Deadlock with Try-and-Release#

One way to avoid deadlock is to acquire both needed resources as a single logical operation. If the second resource is not available, the thread releases the first resource and tries again later.

eat() {
   while(does not have both forks) {
      pickup left fork;
      if(can pick up right fork) {
         pick up right fork;
      } else {
         put down left fork;
      }
   }
   proceed with eating;
}

This avoids the deadlock cycle because a philosopher does not continue holding one fork while waiting forever for the other fork. The tradeoff is that the implementation is more complex and can still need fairness rules to avoid starvation or livelock.

Avoiding Deadlock with Lock Ordering#

Another way to avoid deadlock is to assign a global order to resources and require every thread to acquire resources in that order.

eat() {
   pick up fork with lower number;
   pick up fork with higher number;
   proceed with eating;
}

This works because a global order removes cycles from the lock graph. If all threads acquire the lower-numbered lock before the higher-numbered lock, no thread can create a circular wait.

Directed Lock Graph

Dijkstra’s Solution and the Banker’s Algorithm#

Dijkstra’s solution to the dining philosophers problem uses resource ordering. The same basic idea appears in the Banker’s algorithm: do not enter a resource allocation state that can lead to deadlock.

The main advantage is simplicity. Lock ordering is easy to state, easy to review, and often easy to implement. A common approach is to lock mutexes in address order or in the order they appear in a table.

The main disadvantage is reduced concurrency. Some lock acquisition orders would be safe in a specific execution, but a strict global order forbids them anyway.

Deadlock Avoidance Implementation#

Deadlock avoidance prevents unsafe lock acquisition by enforcing a consistent order.

typedef struct {
   int LockNumber;
   void* LockObject;
} lock;

void multi_lock(lock* locks, int count) {
   sort(locks, count);
   for(int i = 0; i < count; i++) {
      lock(locks[i]);
   }
}

The important part is not the sorting function itself. The important part is that every caller uses the same ordering rule before acquiring more than one lock.

Deadlock Prevention Implementation#

Deadlock prevention can also be done by acquiring locks with a try-and-release rule. If any lock cannot be acquired, the thread releases what it already holds and tries again.

typedef struct {
   int LockNumber;
   void* LockObject;
} lock;

void multi_lock(lock* locks, int count) {
   while(1) {
      int i = 0;
      for(i = 0; i < count; i++) {
         if(!try_lock(locks[i])) {
            for(int j = 0; j < i; j++) {
               unlock(locks[j]);
            }
            break;
         }
      }
      if(i == count) {
         return;
      }
   }
}

This approach can allow more concurrency than strict ordering, but it can also be harder to reason about. Without backoff or fairness, two threads can repeatedly interfere with each other.

Dining Philosophers Example#

The systems-code-examples/dining-philosophers example can run with or without resource ordering. The default mode can deadlock. The DINING_POLICY=avoid_deadlock mode reorders forks to avoid the cycle.

  1#include <pthread.h>
  2#include <stdio.h>
  3#include <stdlib.h>
  4#include <unistd.h>
  5
  6#include "diners.h"
  7#include "millisleep.h"
  8
  9#include <stdio.h>
 10#include <stdlib.h>
 11#include <unistd.h>
 12
 13#define MAX_DINERS 5
 14#define MAX_THINK_TIME 7
 15#define MAX_EAT_TIME 7
 16#define MAIN_THREAD_SLEEP_TIME 51
 17
 18typedef struct {
 19    int num_philosophers;
 20    int think_time;
 21    int eat_time;
 22    int enumerate_resources;
 23} Configuration;
 24
 25void parseCommandLine(int argc, char **argv, Configuration *config) {
 26    int opt;
 27
 28    while ((opt = getopt(argc, argv, "n:t:e:r")) != -1) {
 29        switch (opt) {
 30            case 'n': // Number of philosophers
 31                config->num_philosophers = atoi(optarg);
 32                if (config->num_philosophers > MAX_DINERS || config->num_philosophers < 1) {
 33                    fprintf(stderr, "Number of philosophers must be between 1 and %d\n", MAX_DINERS);
 34                    exit(EXIT_FAILURE);
 35                }
 36                break;
 37            case 't': // Think time
 38                config->think_time = atoi(optarg);
 39                if (config->think_time > MAX_THINK_TIME || config->think_time < 0) {
 40                    fprintf(stderr, "Think time must be between 0 and %d seconds\n", MAX_THINK_TIME);
 41                    exit(EXIT_FAILURE);
 42                }
 43                break;
 44            case 'e': // Eat time
 45                config->eat_time = atoi(optarg);
 46                if (config->eat_time > MAX_EAT_TIME || config->eat_time < 0) {
 47                    fprintf(stderr, "Eat time must be between 0 and %d seconds\n", MAX_EAT_TIME);
 48                    exit(EXIT_FAILURE);
 49                }
 50                break;
 51            case 'r': // Enumerate resources
 52                config->enumerate_resources = 1;
 53                break;
 54            default: /* '?' */
 55                fprintf(stderr, "Usage: %s [-n num_philosophers] [-t think_time] [-e eat_time] [-r]\n",
 56                        argv[0]);
 57                exit(EXIT_FAILURE);
 58        }
 59    }
 60}
 61
 62int main(int argc, char **argv) {
 63    Configuration config = {
 64        .num_philosophers = 5,      // default number of philosophers
 65        .think_time = 2,            // default think time in seconds
 66        .eat_time = 3,              // default eat time in seconds
 67        .enumerate_resources = 0    // default for resource enumeration: off
 68    };
 69
 70    parseCommandLine(argc, argv, &config);
 71
 72    printf("Number of Philosophers: %d\n", config.num_philosophers);
 73    printf("Think Time: %d seconds\n", config.think_time);
 74    printf("Eat Time: %d seconds\n", config.eat_time);
 75    printf("Enumerate Resources: %s\n", config.enumerate_resources ? "Yes" : "No");
 76
 77    fork_t fork[MAX_DINERS];
 78    diner_t diner[MAX_DINERS];
 79
 80    /* The main thread  */
 81    pthread_t this_thread = pthread_self();
 82    struct sched_param sched_parameters;
 83    sched_parameters.sched_priority = sched_get_priority_max(SCHED_OTHER);
 84    int status = pthread_setschedparam(this_thread, SCHED_OTHER, &sched_parameters);
 85
 86    // the main thread monitors the state of the diners and runs at highest
 87    // priority so it can continue doing something, even if (or when) the other
 88    // threads get deadlocked.
 89
 90    if (status != 0)
 91    {
 92        fprintf(stderr, "Cannot set main thread max prioirty. Fatal.\n");
 93        exit(1);
 94    }
 95
 96    /* initialize the shared resources (forks) */
 97    for (int i = 0; i < MAX_DINERS; i++)
 98        fork_init(&fork[i], i);
 99
100    /* initialize the diners to link to the shared resources (forks) */
101    for (int i = 0; i < MAX_DINERS; i++)
102    {
103        int id1 = i;
104        int id2 = (i + 1) % MAX_DINERS;
105        if (get_dining_policy() == FORK_REORDERING && id1 > id2)
106        {
107            printf("Reordered forks %d and %d\n", id1, id2);
108            diner_init(&diner[i], i, &fork[id2], &fork[id1]);
109        }
110        else
111        {
112            diner_init(&diner[i], i, &fork[id1], &fork[id2]);
113        }
114    }
115
116    /* start the actual threads for each diner */
117    for (int i = 0; i < MAX_DINERS; i++)
118    {
119        printf("Creating thread %d", i);
120        diner_start(&diner[i]);
121    }
122
123    /* monitor the state of the diners in the main (highest-priority) thread
124     * because it is high priority, it must sleep for a short time to give
125     * the diner threads a chance to do their thing.
126     */
127    int go_on = 1;
128    while (go_on)
129    {
130        for (int i = 0; i < MAX_DINERS; i++)
131        {
132            putchar(diner[i].state);
133        }
134        putchar('\n');
135        go_on = 0;
136        for (int i = 0; i < MAX_DINERS; i++)
137        {
138            go_on |= diner[i].state != 'd';
139        }
140        millisecond_sleep(MAIN_THREAD_SLEEP_TIME);
141    }
142
143    /* assuming no deadlock happened, this code will be reached so we
144     * can join with the main thread
145     */
146
147    for (int i=0; i < MAX_DINERS; i++)
148    {
149        diner_await(&diner[i]);
150        printf("Diner id %c exited normally; state = %c\n", diner[i].id, diner[i].state);
151    }
152
153    /* locks associated with forks need to be cleaned up */
154    for (int i = 0; i < MAX_DINERS; i++)
155        fork_free_resources(&fork[i]);
156
157    /* attribute object should also be freed (could do earlier, too) */
158    //pthread_attr_destroy(&attr);
159}

Key points:

  • The program creates five shared fork_t objects and five diner_t objects.

  • Each diner receives pointers to the two forks it needs.

  • The default order gives each diner its normal left and right fork.

  • When the policy is avoid_deadlock, the program reorders the fork pointers so each diner follows the same resource order.

  • The main thread prints each diner’s state so deadlock can be observed.

  1//
  2// Created by gkt on 10/16/20.
  3//
  4
  5#include <string.h>
  6#include <stdlib.h>
  7#include <pthread.h>
  8
  9#include "diners.h"
 10#include "millisleep.h"
 11
 12void fork_init(fork_t *fork, int value)
 13{
 14    fork->id = '0' + value;
 15    pthread_mutex_init(&fork->lock, NULL);
 16}
 17
 18void fork_pickup(fork_t *fork)
 19{
 20    pthread_mutex_lock(&fork->lock);
 21}
 22
 23void fork_putdown(fork_t *fork)
 24{
 25    pthread_mutex_unlock(&fork->lock);
 26}
 27
 28void fork_free_resources(fork_t *fork)
 29{
 30    pthread_mutex_destroy(&fork->lock);
 31}
 32
 33void diner_init(diner_t *diner, int id, fork_t *left, fork_t *right)
 34{
 35    diner->id = '0' + id;
 36    diner->state = 't';
 37    diner->left = left;
 38    diner->right = right;
 39}
 40
 41void diner_start(diner_t *diner)
 42{
 43    pthread_attr_t attr;
 44
 45    pthread_attr_init(&attr);
 46    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
 47    pthread_create(&diner->thread, &attr, diner_run, (void *)diner);
 48    pthread_attr_destroy(&attr);
 49}
 50
 51void diner_await(diner_t *diner)
 52{
 53    void *result;
 54    pthread_join(diner->thread, &result);
 55}
 56
 57void diner_think(diner_t *diner)
 58{
 59    diner->state = 't';
 60    millisecond_sleep(rand() % MAX_THINK_TIME);
 61}
 62
 63void diner_eat(diner_t *diner)
 64{
 65    diner->state = 'e';
 66    millisecond_sleep(rand() % MAX_EAT_TIME);
 67}
 68
 69void *diner_run(void *tsd)    /* tsd should be the diner object */
 70{
 71    diner_t *diner = (diner_t *) tsd;
 72
 73    for (int i = 0; i < 1000; i++)
 74    {
 75
 76        diner_think(diner);
 77        diner->state = diner->left->id;
 78        millisecond_sleep(1);
 79        fork_pickup(diner->left);
 80        diner->state = diner->right->id;
 81        millisecond_sleep(1);
 82        fork_pickup(diner->right);
 83        diner_eat(diner);
 84        fork_putdown(diner->left);
 85        fork_putdown(diner->right);
 86    }
 87    diner->state = 'd';
 88    pthread_exit(diner);
 89}
 90
 91enum dining_policy_t get_dining_policy()
 92{
 93    char *dining_policy = getenv("DINING_POLICY");
 94    if (dining_policy == NULL)
 95        return NO_FORK_REORDERING;
 96
 97    if (strcmp(dining_policy, "avoid_deadlock") == 0)
 98        return FORK_REORDERING;
 99
100    return NO_FORK_REORDERING;
101}

Key points:

  • Each fork is protected by a pthread_mutex_t.

  • fork_pickup() locks the fork mutex, and fork_putdown() unlocks it.

  • diner_run() repeatedly thinks, picks up the left fork, picks up the right fork, eats, and releases both forks.

  • The short sleep between fork acquisitions makes the deadlock easier to reproduce.

  • get_dining_policy() reads the environment variable that selects the deadlock-avoidance policy.

 1//
 2// Created by gkt on 10/16/20.
 3//
 4
 5#ifndef DINERS_H
 6#define DINERS_H
 7
 8#define MAX_DINERS 5
 9#define MAX_THINK_TIME 7
10#define MAX_EAT_TIME 7
11#define MAIN_THREAD_SLEEP_TIME 51
12
13typedef struct
14{
15    char id;
16    pthread_mutex_t lock;
17} fork_t;
18
19typedef struct
20{
21    char id;
22    char state;
23    fork_t *left;
24    fork_t *right;
25    pthread_t thread;
26} diner_t;
27
28extern void fork_init(fork_t *fork, int value);
29
30extern void fork_pickup(fork_t *fork);
31
32extern void fork_putdown(fork_t *fork);
33
34extern void fork_free_resources(fork_t *fork);
35
36extern void diner_init(diner_t *diner, int id, fork_t *left, fork_t *right);
37
38extern void diner_think(diner_t *diner);
39
40extern void diner_eat(diner_t *diner);
41
42extern void diner_start(diner_t *diner);
43
44extern void diner_await(diner_t *diner);
45
46extern void *diner_run(void *tsd);
47
48enum dining_policy_t
49{
50    NO_FORK_REORDERING, FORK_REORDERING
51};
52
53extern enum dining_policy_t get_dining_policy();
54
55#endif //DINERS_DININGPHILOSOPHERS_H

Key points:

  • fork_t stores the fork id and the mutex protecting that fork.

  • diner_t stores the diner id, current state, fork pointers, and pthread handle.

  • The dining_policy_t enum records whether fork reordering is active.

  • The header separates the shared data model from the implementation in diners.c.

Example of Deadlock in Nested Calls#

Deadlock can be harder to see when locks are acquired through nested method calls.

void method1() {
   a.lock();
   method2();
   a.unlock();
}
void method2() {
   b.lock();
   c.lock();
   b.unlock();
   c.unlock();
}
void method3() {
   c.lock();
   method2();
   method4();
   c.unlock();
}
void method4() {
   d.lock();
   d.unlock();
}

Here, method3() locks c and then calls method2(), which tries to lock c again after locking b. Depending on the lock type and the rest of the program, this can produce self-deadlock or contribute to a larger lock-ordering cycle.

Multi-Lock Solutions in Windows#

Windows provides WaitForMultipleObjects() for waiting on multiple kernel objects.

The objects can include events, mutexes, semaphores, timers, and other waitable handles. This gives Windows programs a system-level way to wait for one or more synchronization objects.

Multi-Lock Solutions in Linux and Minix#

Linux and Minix do not provide a general multi-lock operation for normal pthread mutexes.

The usual guidance is to avoid multi-locking when possible. When multiple locks are necessary, use a consistent ordering rule. For shared-memory semaphores, virtual address order is not reliable across processes, so the order should come from a shared table, array index, or explicit lock number.

Deadlock with Correct Lock Ordering#

Lock ordering prevents circular wait, but it does not prevent every way a program can stop making progress.

A thread can still lock a resource and fail to release it. Common causes include missing unlock calls, crashes while holding a lock, infinite loops inside critical sections, and monitor conditions that never become true. These failures are often harder to handle because the lock order may be correct while the program state is not.

Starvation#

Starvation occurs when one or more threads wait indefinitely because other threads keep getting access to the resource first.

Starvation is closely related to scheduling fairness. A program can avoid deadlock and still be unfair enough that one thread makes little or no progress.

Starvation Example#

This example holds a lock while performing blocking I/O. That can make other threads wait much longer than necessary.

Queue _queue = new Queue();
Mutex _mutex = new Mutex();

void add() {
   int value = 0;
   while(1) {
      _mutex->Lock();
      while(_queue->count() > 0) {
         value += _queue->Dequeue();
         printf("current value = %d\n", value);
      }
      _mutex->Unlock();
   }
}
void read_values() {
   while(1) {
      int value = 0;
      _mutex->Lock();
      scanf("%d\n", &value);
      _queue->Enqueue(value);
      _mutex->Unlock();
   }
}

The first thread holds the lock while calling printf(). The second thread holds the lock while calling scanf(). Both operations can block, and while either one blocks the other thread cannot enter the critical section.

Reducing Starvation#

One simple improvement is to keep blocking I/O outside the critical section.

Queue _queue = new Queue();
Mutex _mutex = new Mutex();

void add() {
   int value = 0;
   while(1) {
      _mutex->Lock();
      while(_queue->count() > 0) {
         value += _queue->Dequeue();
         _mutex->Unlock();
         printf("current value = %d\n", value);
         _mutex->Lock();
      }
      _mutex->Unlock();
   }
}
void read_values() {
   while(1) {
      int value = 0;
      scanf("%d\n", &value);
      _mutex->Lock();
      _queue->Enqueue(value);
      _mutex->Unlock();
   }
}

This version does not hold the lock during printf() or scanf(). The critical section is shorter, and the average wait time for the lock is reduced.

Guidelines to Avoid Starvation#

Starvation is less likely when locks are held for short, predictable periods.

Useful guidelines include:

  • Keep critical sections short.

  • Avoid blocking I/O while holding a lock.

  • If a computation is long-running, design it so the lock can be released periodically.

  • Copy the needed data while holding the lock, then do expensive work after releasing it.

  • Use synchronization libraries that provide fairness guarantees when fairness matters.

Livelock#

Livelock occurs when threads keep running but repeatedly take actions that prevent progress.

It can happen in deadlock-avoidance code when two threads both release and retry at the same time.

void thread1() {
   while(1) {
      a.lock();
      if(b.trylock()) {
         /* do work */
         b.unlock();
      }
      a.unlock();
   }
}
void thread2() {
   while(1) {
      b.lock();
      if(a.trylock()) {
         /* do work */
         a.unlock();
      }
      b.unlock();
   }
}

One possible sequence is:

  • thread1 locks a.

  • thread2 locks b and fails to lock a.

  • thread1 fails to lock b and releases a.

  • thread2 releases b.

If the pattern repeats, both threads continue executing but neither completes the protected work. Backoff, ordering, or a coordinator can break the livelock.

Lock Fairness#

Lock fairness means that threads waiting for a lock have similar average wait times.

Fair locks can reduce starvation and make execution more predictable. Common approaches include FIFO wait queues and, less commonly, schedulers that use timing history.

Fairness Tradeoffs#

Fairness has a cost because selecting the next owner of a lock is not the same as immediately running that thread.

A fair lock can reduce throughput when a thread repeatedly locks and unlocks a short critical section. An unfair lock may let the same thread reacquire the lock quickly, which can improve locality and throughput but can also starve other threads.