20 November 2012

Understand and Prevent Deadlocks


Can you explain a typical C# deadlock in a few words? Do you know the simple rules that help you to write deadlock free code? Yes? Then stop reading and do something more useful.

If several threads have read/write access to the same data it is often often necessary to limit access to only on thread. This can be done with C# lock statement. Only one thread can execute code that is protected by a lock statement and a lock object. It is important to understand that not the lock statement protects the code, but the object given as an argument to the lock statement. If you don't know how the lock statement works, please read the msdn documentation before continuing. Using a lock statement is better than directly using a Mutex or EventWaitHandle because it protects you from stale locks that can occur if you forget to release your lock when an exception happens.

A deadlock can occur only if you use more than one lock object and the locks are acquired by each thread in a different order. Look at the following sequence diagram:



There are two threads A and B and two resources X and Y. Each resource is protected by a lock object.
Thread A acquires a lock for Resource X and continues. Then Thread B acquires a lock for Y and continues. Now Thread A tries to acquire a lock for Y. But Y is already locked by Thread B. This means Thread A is blocked now and waits until Y is released. Meanwhile Thread B continues and now needs a lock for X. But X is already locked by Thread B. Now Thread A is waiting for Thread B and Thread B is waiting for Thread A both threads will wait forever. Deadlock!

The corresponding code could look like this.

public class Deadlock
{
    static readonly object X = new object();
    static readonly object Y = new object();
   
    public void ThreadA()
    {
        lock(X)
        {           
            lock(Y)
            {
                // do something
            }
        }
    }

    public void ThreadB()
    {
        lock(Y)
        {
            lock(X)
            {
                // do something
            }
        }
    }
}

Normally nobody will write code as above with obvious deadlocks. But look at the following code, which is deadlock free:


public class Deadlock
{
    static readonly object X = new object();
    static readonly object Y = new object();
    static object _resourceX;
    static object _resourceY;

    public object ResourceX
    {
        get { lock (X) return _resourceX; }
    }

    public object ResourceY
    {
        get
        {
            lock (Y)
            {
                return _resourceY ?? (_resourceY = "Y");
            }
        }
    }

    public void ThreadA()
    {
        Console.WriteLine(ResourceX);
    }

    public void ThreadB()
    {
        lock(Y)
        {
            _resourceY = "TEST";
            Console.WriteLine(ResourceX);
        }
    }
}


But after re-factoring the getter for ResourceX to this

get { lock (X) return _resourceX ?? ResourceY; }

you have the same deadlock as in the first code sample!

Deadlock prevention rules


  1. Don't use static fields. Without static fields there is no need for locks.
  2. Don't reinvent the wheel. Use thread safe data structures from System.Collections.Concurrent or System.Threading.Interlocked before pouring lock statements over your code.
  3. A lock statement must be short in code and time. The lock should last nanoseconds not milliseconds.
  4. Don't call external code inside a lock block. Try to move this code outside the lock block. Only the manipulation of known private data should be protected. You don't know if external code contains locks now or in future (think of refactoring).

If you are following these rules you have a good chance to never introduce a deadlock in your whole career.



No comments:

Post a Comment