Deadlocks

Please work out the pre-lab problems before your next lab section. The GSI/IAs will go over all problems during the lab section.

[pre-lab] 1. Preventing deadlock

You have learned the four necessary conditions for deadlock:

Deadlock prevention schemes work by attempting to remove one (or more) of these conditions. In this question, you will look at two such methods.

You are developing a multithreaded bank software package, which has a procedure for transferring funds from one account to another. Each user account has a lock associated with it.

You must acquire the lock for both accounts before transferring funds, to ensure the money transfer is atomic. The transfer function prototype is:

transfer_money( int from_account_id, int to_account_id, int amount );

Here is an incorrect solution:

/*
 * An array of locks that ranges over account numbers.
 * locks[i] = lock for account i
 */
vector locks;

void transfer_money(int acct1, int acct2, int amount) {
    thread_lock(locks[acct1]);
    thread_lock(locks[acct2]);
    <transfer money>
    thread_unlock(locks[acct2]);
    thread_unlock(locks[acct1]);
}
For example, thread 1 could execute transfer_money(1, 2, 20), and thread 2 could execute transfer_money(2, 1, 100).

A. How can this deadlock?

B. Attacking the circular wait condition

Rewrite transfer_money() to be deadlock-free, by making a circular wait impossible. (Hint: define an ordering for the locks.)

3. Attacking the hold-and-wait condition

Rewrite transfer_money() again, this time avoiding any possible deadlock by removing the hold-and-wait condition. (Hint: each thread should either acquire both locks at once, or not at all). You may define new shared variables and/or new locks and condition variables. Your solution should not busy-wait.

[in-lab] 2. Debugging deadlock

Consider the following program, which uses C++ threads.

#include <iostream>
#include <thread>
#include <mutex>

std::mutex x, y, c;

void child1()
{
    for (int i=0; i<1000000; i++) {
        if (!(i%100)) {
	    c.lock();
	    std::cout << "child1: " << i << std::endl;
	    c.unlock();
	}
	x.lock();
	y.lock();
	y.unlock();
	x.unlock();
    }
}

void child2()
{
    for (int i=0; i<1000000; i++) {
        if (!(i%100)) {
	    c.lock();
	    std::cout << "child2: " << i << std::endl;
	    c.unlock();
	}
	y.lock();
	x.lock();
	x.unlock();
	y.unlock();
    }
}

main()
{
    std::thread* t[2];

    std::thread t1(child1);
    std::thread t2(child2);

    t1.join();
    t2.join();
}

A. How can this program deadlock?

B. Compile this program (g++ race.cc -pthread) and run it in gdb until it deadlocks. Examine the state of the deadlocked threads in gdb, using commands such as "info threads", "thread ", "bt", and "thread apply all ...". Where is each thread blocked?

C. Now find where each thread is blocked by adding cout statements before/after each potentially blocking call. This technique is useful when a debugger is not available, or when the debugger does not understand your implementation of threads (as in Project 2).