CS110: Principles of Computer Systems

Spring 2021
Instructors Roz Cyrus and Jerry Cain

PDF

Multithreading, Dining Philosophers, Deadlock

  • This is what our dining philosopher's solution looked like at the end of Wednesday's lecture.  We intentionally inserted a sleep_for(5000) call in the worst possible place to highlight the deadlock threat that must be removed.
static void eat(size_t id, mutex& left, mutex& right) {
  left.lock();
  sleep_for(5000);  // artificially force off the processor
  right.lock();
  cout << oslock << id << " starts eating om nom nom nom." << endl << osunlock;
  sleep_for(getEatTime());
  cout << oslock << id << " all done eating." << endl << osunlock;
  left.unlock();
  right.unlock();
}

static void philosopher(size_t id, mutex& left, mutex& right) {
  for (size_t i = 0; i < 3; i++) {
    think(id); // not implemented because thinking is easy
    eat(id, left, right);
  }
}

int main(int argc, const char *argv[]) {
  mutex forks[5];
  thread philosophers[5];
  for (size_t i = 0; i < 5; i++) {
    mutex& left = forks[i], & right = forks[(i + 1) % 5];
    philosophers[i] = thread(philosopher, i, ref(left), ref(right));
  }
  for (thread& p: philosophers) p.join();
  return 0;
}

Multithreading, Dining Philosophers, Deadlock

  • When coding with threads, you need to ensure that:
    • there are no race conditions, even if they rarely cause problems, and
    • there's zero threat of deadlock, lest a subset of threads forever starve for processor time.
  • mutexes are generally the solution to race conditions, as we've seen with the ticket agent example. We can use them to mark the boundaries of critical regions and limit the number of threads present within them to be at most one.
  • Deadlock can be programmatically prevented by implanting directives to limit the number of threads competing for a shared resource, like, you know, utensils.
    • We could, for instance, recognize that it's impossible for three philosophers to be eating at the same time. That means we could limit the number of philosophers who have permission to grab forks to a mere 2.
    • We could also argue it's okay to let four—though certainly not all five—philosophers grab forks, knowing that at least one will successfully grab both.
      • My personal preference? Impose a limit of four.
      • My rationale? Implant the minimal amount of bottlenecking needed to remove the threat of deadlock, and trust the OS and thread manager to otherwise make good choices.

Multithreading, Dining Philosophers, Deadlock Removal

  • Here's the core of a program that limits the number of philosophers grabbing forks to four. (The full program can be found right here.)
    • I impose this limit by introducing the notion of a permission slip, or permit. Before grabbing forks, a philosopher must first acquire one of four permission slips.
    • These permission slips need to be acquired and released without race condition.
    • For now, I'll model a permit using a counter—I call it permits—and a companion mutex—I call it permitsLock—that must be acquired before examining or changing permits.
int main(int argc, const char *argv[]) {
  size_t permits = 4;
  mutex forks[5], permitsLock;
  thread philosophers[5];
  for (size_t i = 0; i < 5; i++) {
    mutex& left = forks[i],
         & right = forks[(i + 1) % 5];
    philosophers[i] = 
        thread(philosopher, i, ref(left), ref(right), ref(permits), ref(permitsLock));
  }
  for (thread& p: philosophers) p.join();
  return 0;
}

Multithreading, Dining Philosophers, Deadlock Removal

  • The implementation of think is the same, so I don't present it again.
  • The implementation of eat, however, changes a bit.
    • It accepts two additional references: one to the number of available permits, and a second to the mutex used to guard against simultaneous access to permits.
static void eat(size_t id, mutex& left, mutex& right, size_t& permits, mutex& permitsLock) {
  waitForPermission(permits, permitsLock); // on next slide
  left.lock(); right.lock();
  cout << oslock << id << " starts eating om nom nom nom." << endl << osunlock;
  sleep_for(getEatTime());
  cout << oslock << id << " all done eating." << endl << osunlock;
  grantPermission(permits, permitsLock); // on next slide
  left.unlock(); right.unlock();
}

static void philosopher(size_t id, mutex& left, mutex& right,
                        size_t& permits, mutex& permitsLock) {
  for (size_t i = 0; i < kNumMeals; i++) {
    think(id);
    eat(id, left, right, permits, permitsLock);
  }
}

Multithreading, Dining Philosophers, Deadlock Removal

  • The implementation of eat on the prior slide deck introduces calls to waitForPermission and grantPermission.
    • The implementation of grantPermission is certainly the easier of the two to understand: transactionally increment the number of permits by one.
    • The implementation of waitForPermission is less obvious. Because we don't know what else to do (yet!), we busy wait with short naps until permits goes positive. Once that happens, we consume a permit and return.
static void waitForPermission(size_t& permits, mutex& permitsLock) {
  while (true) {
    permitsLock.lock();
    if (permits > 0) break;
    permitsLock.unlock();
    sleep_for(10);
  }
  permits--;
  permitsLock.unlock();
}

static void grantPermission(size_t& permits, mutex& permitsLock) {
  permitsLock.lock();
  permits++;
  permitsLock.unlock();
}

Dining Philosophers, Deadlock Removal, Busy Waiting

  • The second version of the program works, in the sense that it never deadlocks.
    • It does, however, suffer from busy waiting, which the systems programmer gospel says is verboten unless there are no other options.
  • A better solution? If a philosopher doesn't have permission to advance, then that thread should sleep until another thread sees reason to wake it up. In this example, another philosopher thread, after it increments permits within grantPermission, could notify the sleeping thread that a permit just became available.
  • Implementing this idea requires a more sophisticated concurrency directive that supports a different form of thread communication—one akin to the use of signals and sigsuspend to support communication between processes. Fortunately, C++ provides a standard directive called the condition_variable_any to do exactly this.
class condition_variable_any {
public:
   void wait(mutex& m);
   template <typename Pred> void wait(mutex& m, Pred pred);
   void notify_one();
   void notify_all();
};

Lecture 11 Pre: Condition Variables and Semaphores

By Jerry Cain

Lecture 11 Pre: Condition Variables and Semaphores

  • 1,153