IC221: Systems Programming (SP14)


Home Policy Calendar Syllabus Resources Piazza

Lec 26: Deadlock Avoidance

Table of Contents

1 Deadlocks Review

Recall from the previous lecture that when we use fine grain locking, there is a potential for deadlock. Where two threads hold the resource the other needs. An example of this is in the following code:

#include <stdio.h>
#include <stdlib.h>

#include <pthread.h>

pthread_mutex_t lock_a, lock_b;

void * fun_1(void * arg){
  int i;
  for (i = 0 ; i< 10000 ; i++){

    pthread_mutex_lock(&lock_a); //lock a then b
    pthread_mutex_lock(&lock_b);

    //CRITICAL SECTION

    pthread_mutex_unlock(&lock_a);
    pthread_mutex_unlock(&lock_b);

  }

  return NULL;
}

void * fun_2(void * arg){
  int i;
  for (i = 0 ; i< 10000 ; i++){

    pthread_mutex_lock(&lock_b); //lock b then a
    pthread_mutex_lock(&lock_a);

    //CRITICAL SECTION

    pthread_mutex_unlock(&lock_b);
    pthread_mutex_unlock(&lock_a);

  }

  return NULL;
}

int main(){

  pthread_t thread_1,thread_2;

  pthread_mutex_init(&lock_a, NULL);
  pthread_mutex_init(&lock_b, NULL);

  pthread_create(&thread_1, NULL, fun_1, NULL);
  pthread_create(&thread_2, NULL, fun_2, NULL);

  pthread_join(thread_1, NULL);
  pthread_join(thread_2, NULL);

  return 0;
}

In this code we have two threads and two locks. One thread acquires lock_a and then lock_b while the other thread acquires lock_b and then lock_a. If we run this program we find that nothing happens; the program hangs. This is because of a deadlock.

Consider what happens when one each thread run such that one holds lock_a and the other holds lock_b, then one thread is waiting on the lock the other thread holds. Both threads are now blocking on acquiring the resources, and the main thread is blocking trying to join the worker threads. Nothing is running: deadlock.

2 Avoiding Deadlocks

Deadlocks are very hard to debug and are a big part of concurrent programming. The best way to avoid deadlocks is to ensure that all locking for all threads occur in the same order. That may seam like a simple task, but it is not because the notion of ordering is not always clear. Let's look at a slightly more complicated example of the bank where instead of a single account, we have multiple accounts. A thread randonly chooses two accounts and transfer money between them.

   #define INIT_BALANCE 1000
#define NUM_TRANS 100
#define MAX_TRANS 10
#define NUM_ACCOUNTS 10

//create an account
struct account{
  int balance;
  int credits;
  int debits;
};

//array of accounts
struct account accounts[NUM_ACCOUNTS];


void * transactions(void * args){
  int i,v;
  int a1,a2;

  for(i=0;i<NUM_TRANS;i++){

    //choose a random value
    v = (int) random() % NUM_TRANS;

    //choose two random accounts
    a1 = (int) random() % NUM_ACCOUNTS;
    while((a2 = (int) random() % NUM_ACCOUNTS) == a1);

    //transfer from a1 to a2
    accounts[a1].balance += v;
    accounts[a1].credits  += v;

    accounts[a2].balance -= v;
    accounts[a2].debits += v;
  }

  return 0;
}

If we run this program, since we have no locking, we run into the same situation as before, inconsistent output. Let's look at how we might want to add locks to the program. The simplest means to do this is to use fine locking at the level accounts.

2.1 Lock Acquisition Issues

By adding a lock to each account, we now require a worker thread to acquire the account lock on both accounts before executing a transfer:

//create an account
struct account{
  int balance;
  int credits;
  int debits;
  pthread_mutex_t lock; //lock for the account
};

//array of accounts
struct account accounts[NUM_ACCOUNTS];


void * transactions(void * args){
  int i,v;
  int a1,a2;

  for(i=0;i<NUM_TRANS;i++){

    //choose a random value
    v = (int) random() % NUM_TRANS;

    //choose two random accounts
    a1 = (int) random() % NUM_ACCOUNTS;
    while((a2 = (int) random() % NUM_ACCOUNTS) == a1);

    //transfer from a1 to a2
    //    printf("Transfer %d->%d : %d \n", a1,a2,v);

    //acquire lock for a1 then a2
    pthread_mutex_lock(&accounts[a1].lock);
    pthread_mutex_lock(&accounts[a2].lock);


    accounts[a1].balance += v;
    accounts[a1].credits  += v;

    accounts[a2].balance -= v;
    accounts[a2].debits += v;

    pthread_mutex_unlock(&accounts[a1].lock);    
    pthread_mutex_unlock(&accounts[a2].lock);

  }

  return 0;
}

This might seem to work, but this locking strategy actually will form a deadlock. Why? The locking strategy is to lock a1 then a2 always in that order, how could a deadlock occur?

Consider that the accounts are chosen randomly from all accounts and that there are multiple threads running. The variables a1 and a2 are standin's for account numbers or indexes into the array of accounts. What if in two threads the following happens?

Thread 1        Thread 2
---------      ----------
a1 = 5;          a1 = 3;
a2 = 3;          a2 = 5;

Then when we look at lock ordering, we find the following situation and deadlock might occur:

Thread 1        Thread 2
---------      ----------
lock( 5 )       
                lock( 3 )
lock( 3 )       
                lock( 5 )


        DEADLOCK

We find ourselves back where we started, each thread is now waiting on a lock held by another thread. Progress cannot be made and a deadlock occurs. This happens because we did not apply a proper order to locking: How do we apply a proper ordering?

2.2 Ordering Resources to Avoid Deadlock

One way to apply a proper ordering is to leverage the fact that the account indexes are already ordered. What if we always lock accounts in account order? Then in above example, both threads would do lock(3) then lock(5). In code, this would look like this:

void * transactions(void * args){
  int i,v;
  int a1,a2;

  for(i=0;i<NUM_TRANS;i++){

    //choose a random value
    v = (int) random() % NUM_TRANS;

    //choose two random accounts
    a1 = (int) random() % NUM_ACCOUNTS;
    while((a2 = (int) random() % NUM_ACCOUNTS) == a1);

    //transfer from a1 to a2

    if(a1 < a2){
      pthread_mutex_lock(&accounts[a1].lock);
      pthread_mutex_lock(&accounts[a2].lock);
    }else{
      pthread_mutex_lock(&accounts[a2].lock);
      pthread_mutex_lock(&accounts[a1].lock);
    }

    accounts[a1].balance += v;
    accounts[a1].credits  += v;

    accounts[a2].balance -= v;
    accounts[a2].debits += v;

    if(a1 < a2){
      pthread_mutex_unlock(&accounts[a2].lock);
      pthread_mutex_unlock(&accounts[a1].lock);
    }else{
      pthread_mutex_unlock(&accounts[a1].lock);
      pthread_mutex_unlock(&accounts[a2].lock);
    }
  }

  return 0;
}

Now, everything is consistent and deadlock is avoided.

3 EXERCISE: The Dining Philosophers

The Dining philosophers is a classic problem of deadlock avoidance. The story goes that a philosopher does only two things in life, eat or philosophize. This particular group of philosophers sits at a large round table with food in front of each of them, and between each philosopher is a single fork. When a philosopher wishes to eat, they must acquire the fork to their right and their left, but those forks are also shared with the adjacent philosopher. If an adjacent philosopher is currently eating, then the hungry philosopher must wait for the other to finish before acquiring the fork. Once a fork is acquired, a philosopher must hold on to it until they are finished eating.

Let's look at a code example of how this might be implemented using threading:

#include <stdio.h>
#include <stdlib.h>

#include <unistd.h>
#include <sys/types.h>

#include <pthread.h>

#define NUM_PHIL 10
#define ROUNDS 1000

struct fork{
  int id;
  pthread_mutex_t * lock; //the lock on the fork
};


struct philosopher{
  int id;               //id of this philosopher
  struct fork * left;   //the left fork
  struct fork * right;  //the right fork
  pthread_t thread_id; //POSIX thread id
};

struct fork forks[NUM_PHIL];
struct philosopher philosophers[NUM_PHIL];

void  * eat(void * args){
  struct philosopher * phil  = (struct philosopher *) args;
  int i;

  for (i = 0;i<ROUNDS;i++){
    //TODO: fix locking to avoid deadlock 

    printf("Philosopher %d ACQUIRING left fork %d\n", phil->id, phil->left->id);
    pthread_mutex_lock(phil->left->lock);

    printf("Philosopher %d ACQUIRING right fork %d\n", phil->id, phil->right->id);
    pthread_mutex_lock(phil->right->lock);

    printf("Philosopher %d EATING with fork %d and %d\n", phil->id, phil->left->id, phil->right->id);
    usleep(250); //sleep for a quarter second

    printf("Philosopher %d RELEASING fork %d\n", phil->id, phil->left->id);
    pthread_mutex_unlock(phil->left->lock);
    printf("Philosopher %d RELEASING fork %d\n", phil->id, phil->right->id);
    pthread_mutex_unlock(phil->right->lock);
  }

  return NULL;
}

int main(){

  int i;

  printf("Initializing forks: %d\n", NUM_PHIL-1);
  //initialize the forks
  for(i=0;i<NUM_PHIL;i++){
    forks[i].id = i;
    forks[i].lock = malloc(sizeof(pthread_mutex_t));
    pthread_mutex_init(forks[i].lock, NULL);
  }

  printf("Initializing philosophers: %d\n", NUM_PHIL);
  //initialize the philosophers
  for(i=0;i<NUM_PHIL;i++){
    philosophers[i].id = i;
    if( i == 0){
      philosophers[i].left = &forks[(NUM_PHIL-1)];
    }else{
      philosophers[i].left = &forks[i-1];
    }

    philosophers[i].right = &forks[(i) % NUM_PHIL];
  }


  printf("Starting Threads\n");
  //start the threads
  for(i=0;i<NUM_PHIL;i++){
    pthread_create(&(philosophers[i].thread_id), NULL,
                   eat, (void *) &(philosophers[i]));
  }

  //join the threads
  for(i=0;i<NUM_PHIL;i++){
    pthread_join(philosophers[i].thread_id, NULL);
  }

  printf("All threads joined\n");
  //clean memory
  for(i=0;i<NUM_PHIL-1;i++){
    pthread_mutex_destroy(forks[i].lock);
    free(forks[i].lock);
  }

  return 0;
}

You'll see we have a structure for philosophers and forks, and that each philosopher has a fork to the right and left. The philosopher with id 0 shares a fork with the philosopher with id NUM_PHIL-1 because they are sitting at a round table.

If we look at the locking procedure more closely, we see that there appears to be reasonable ordering:

printf("Philosopher %d ACQUIRING left fork %d\n", phil->id, phil->left->id);
pthread_mutex_lock(phil->left->lock);

printf("Philosopher %d ACQUIRING right fork %d\n", phil->id, phil->right->id);
pthread_mutex_lock(phil->right->lock);

Every philosopher first reaches left to pick up a fork and then reaches right to pick up a fork. But, it doesn't avoid deadlock.

Your task, working with a partner, is to:

  1. Analyze the program to determine why a deadlock occurs by providing a scenario that causes deadlock.
  2. Provide a solution that avoids deadlock.

DO NOT GOOGLE THE ANSWERS

You must fill in your solution on the lab sheet provided.