Deadlock Practical

Dining Philosophers Rules

  • A philosopher has two states: thinking and eating

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

    • A philosopher can only pick up one fork at a time
    • A philosopher must pick up both forks before they can eat
    • When a philosopher finishes eating, they put down both forks immediately
    • A philosopher may pick up a fork as soon as it is put down by another
  • A philosopher has no awareness of other philosopher's actions

  • If a philosopher does not eat at all, they will die of starvation

  • Intended scenario

    • Philosopher thinks
    • Philosopher picks up left fork
    • Philosopher thinks
    • Philosopher picks up right fork
    • Philosopher eats
    • Philosopher puts down both forks
    • Philosopher thinks

Implementation

  • A separate thread for each philosopher
  • Each fork has an associated mutex

// A mutex prevents more than one philosopher picking up the same fork
std::mutex fork_mutex[nforks];

// A philosopher thread can only pick up a fork if it can lock the corresponding mutex
// Try to pick up the left fork
fork_mutex[lfork].lock();

// Try to pick up the right fork
fork_mutex[rfork].lock();

// Succeeded - the philosopher can now eat


#include <chrono>
#include <iostream>
#include <mutex>
#include <string>
#include <thread>
#include <vector>

using namespace std::literals;

constexpr int nforks = 5;
constexpr int nphilosophers = nforks;
std::string names[nphilosophers] = {"A", "B", "C", "D", "E"};

// Keep track of how many times a philosopher is able to eat
int mouthfuls[nphilosophers] = {0};

// A philosopher who has not picked up both forks is thinking
constexpr std::chrono::duration think_time = 2s;

// A philosopher has picked up both forks is eating
constexpr std::chrono::duration eat_time = 1s;

// A mutex prevents more than one philosopher picking up the same fork
// A philosopher thread can only pick up a fork if it can lock the corresponding
// mutex
std::mutex fork_mutex[nforks];

// Mutex to protect output
std::mutex print_mutex;

// Interactions with a fork
void print(int n, const std::string &str, int forkno) {
  std::lock_guard<std::mutex> print_lck(print_mutex);
  std::cout << "Philosopher " << names[n] << str << forkno << '\n';
}

// The philosopher's state
void print(int n, const std::string &str) {
  std::lock_guard<std::mutex> print_lck(print_mutex);
  std::cout << "Philosopher " << names[n] << str << '\n';
}

// Thread which represents a dining philosopher
void dine(int nphilo) {
  // Philosopher A has fork 0 on their left, and fork 1 on their right
  // Philosopher B has fork 1 on their left, and fork 2 on their right
  // ...
  // Philosopher E has fork 4 on their left, and fork 0 on their right

  // Each philosopher must pick up their left fork first
  int lfork = nphilo;
  int rfork = (nphilo + 1) % nforks;

  print(nphilo, "\'s left fork is number ", lfork);
  print(nphilo, "\'s right fork is number ", rfork);
  print(nphilo, " is thinking...");

  std::this_thread::sleep_for(think_time);

  print(nphilo, " reaches for fork number ", lfork);

  // Try to pick up the left fork
  fork_mutex[lfork].lock();
  print(nphilo, " picks up fork ", lfork);
  print(nphilo, " is thinking...");

  std::this_thread::sleep_for(think_time);

  print(nphilo, " reaches for fork number ", rfork);

  // Try to pick up the right fork
  fork_mutex[rfork].lock();
  print(nphilo, " picks up fork ", rfork);
  print(nphilo, " is eating...");

  std::this_thread::sleep_for(eat_time);

  print(nphilo, " puts down fork ", lfork);
  print(nphilo, " puts down fork ", rfork);
  print(nphilo, " is thinking...");

  fork_mutex[lfork].unlock();
  fork_mutex[rfork].unlock();
  std::this_thread::sleep_for(think_time);
}

int main() {
  // Separate thread for each philosopher
  std::vector<std::thread> philos;
  for (int i = 0; i < nphilosophers; ++i) {
    philos.push_back(std::move(std::thread{dine, i}));
  }

  for (auto &philo : philos) {
    philo.join();
  }

  // How many times were the philosophers able to eat ?
  for (int i = 0; i < nphilosophers; ++i) {
    std::cout << "Philosopher " << names[i];
    std::cout << " had " << mouthfuls[i] << " mouthfuls\n";
  }
}

Deadlock

  • All the philosophers pick up their left fork

  • None of the right forks are available

    • B picks up fork 2
    • Fork 2 is A's right fork
    • A cannot eat without picking up fork 2
    • Fork 2 will not become available until B has finished eating
    • B cannot start eating because C has taken fork 3
  • The philosopher threads are deadlocked

    • The philosophers cannot enter the "eating state"