How to tackle Deadlocks in C#?

Deadlocks are common challenging scenario in multithreaded programming environment developed in C#. A deadlock occurs when more than one thread tries to acquire the shared resource that is held by another thread.

In this article, we will explore the concept of deadlocks, understand their cause and learn effective strategies to prevent them with the examples.

What is deadlock?

Deadlocks take place when multiple threads compete for shared resource like locks or semaphore and each thread waits for the other to release its acquired resources. It creates a circular dependency situation and resulted in deadlock.

Cause 1: Incorrect Locking Order
using System;
using System.Threading;

class Program
{
    static object lock1 = new object();
    static object lock2 = new object();

    static void Main(string[] args)
    {
        Thread thread1 = new Thread(ExecuteThread1);
        Thread thread2 = new Thread(ExecuteThread2);

        thread1.Start();
        thread2.Start();

        thread1.Join();
        thread2.Join();

        Console.WriteLine("Main program has finished.");
    }

    static void ExecuteThread1()
    {
        lock (lock1)
        {
            Console.WriteLine("Thread 1: Holding lock1...");
            Thread.Sleep(100);

            Console.WriteLine("Thread 1: Waiting for lock2...");
            lock (lock2)
            {
                Console.WriteLine("Thread 1: Acquired lock2!");
            }
        }
    }

    static void ExecuteThread2()
    {
        lock (lock2)
        {
            Console.WriteLine("Thread 2: Holding lock2...");
            Thread.Sleep(100);

            Console.WriteLine("Thread 2: Waiting for lock1...");
            lock (lock1)
            {
                Console.WriteLine("Thread 2: Acquired lock1!");
            }
        }
    }
}
Problem Details

In this above scenario, thread1 and thread2 acquire lock1 and lock2 respectively. However, thread1 waits for lock2 after acquiring lock1 while thread2 waits for lock1 after acquiring lock2. This is resulted in deadlock situation.

Solution

To prevent this deadlock situation, make sure the consistent locking order (lock hierarchy) is followed. To achieve this, all the threads acquire the locks in the same order and it will eliminate the possibility of circular dependencies.

static void ExecuteThread1()
{
    lock (lock1)
    {
        Console.WriteLine("Thread 1: Holding lock1...");
        Thread.Sleep(100);

        Console.WriteLine("Thread 1: Waiting for lock2...");

        // Reorder the locks to acquire lock2 after lock1
        lock (lock2)
        {
            Console.WriteLine("Thread 1: Acquired lock2!");
        }
    }
}

static void ExecuteThread2()
{
    // Reorder the locks to acquire lock1 before lock2
    lock (lock1)
    {
        Console.WriteLine("Thread 2: Holding lock1...");
        Thread.Sleep(100);

        Console.WriteLine("Thread 2: Waiting for lock2...");
        lock (lock2)
        {
            Console.WriteLine("Thread 2: Acquired lock2!");
        }
    }
}

In the above solution, thread1 and thread2 try to acquire lock1 before lock2. It ensure the consistency in acquiring the lock in the same order to avoid deadlock situation.

Cause 2 : Nested Locks
using System;
using System.Threading;

class Program
{
    static object lock1 = new object();

    static void Main(string[] args)
    {
        Thread thread1 = new Thread(ExecuteThread1);

        thread1.Start();
        thread1.Join();

        Console.WriteLine("Main program has finished.");
    }

    static void ExecuteThread1()
    {
        lock (lock1)
        {
            Console.WriteLine("Thread 1: Holding lock1...");
            Thread.Sleep(100);

            lock (lock1) // Nested lock (attempting to acquire the same lock again)
            {
                Console.WriteLine("Thread 1: Nested lock - This will lead to deadlock!");
            }
        }
    }
}
Problem Details

In this above scenario, thread1 acquires lock1 but tries to acquire the same lock1 again within the same thread, created a nest lock scenario. Here, Deadlock situation is due to a thread is waiting for the same lock after acquiring the same lock i.e. it waits for itself to release the lock.

Solution

To prevent this nested lock situation, we need to refactor this code and make sure the thread is not try to acquire the same lock again within the same execution path.

static void ExecuteThread1()
{
    lock (lock1)
    {
        Console.WriteLine("Thread 1: Holding lock1...");
        Thread.Sleep(100);
    }

    // The nested lock has been moved outside the first block
    lock (lock1)
    {
        Console.WriteLine("Thread 1: Acquired lock1!");
    }
}

In this solution, The nest lock for the same lock has been removed outside the first block to avoid circular dependencies.

Cause 3: Lock Timeout
using System;
using System.Threading;

class Program
{
    static object lock1 = new object();
    static object lock2 = new object();

    static void Main(string[] args)
    {
        Thread thread1 = new Thread(ExecuteThread1);
        Thread thread2 = new Thread(ExecuteThread2);

        thread1.Start();
        thread2.Start();

        thread1.Join();
        thread2.Join();

        Console.WriteLine("Main program has finished.");
    }

    static void ExecuteThread1()
    {
        lock (lock1)
        {
            Console.WriteLine("Thread 1: Holding lock1...");
            Thread.Sleep(100);

            // Waiting for lock2 with timeout 1 millisecond
            if (Monitor.TryEnter(lock2, TimeSpan.FromMilliseconds(1)))
            {
                try
                {
                    Console.WriteLine("Thread 1: Acquired lock2!");
                }
                finally
                {
                    Monitor.Exit(lock2);
                }
            }
            else
            {
                Console.WriteLine("Thread 1: Failed to acquire lock2 within 1 millisecond.");
            }
        }
    }

    static void ExecuteThread2()
    {
        lock (lock2)
        {
            Console.WriteLine("Thread 2: Holding lock2...");
            Thread.Sleep(100);

            // Waiting for lock1 with timeout 1 millisecond
            if (Monitor.TryEnter(lock1, TimeSpan.FromMilliseconds(1)))
            {
                try
                {
                    Console.WriteLine("Thread 2: Acquired lock1!");
                }
                finally
                {
                    Monitor.Exit(lock1);
                }
            }
            else
            {
                Console.WriteLine("Thread 2: Failed to acquire lock1 within 1 millisecond.");
            }
        }
    }
}
Problem Details

In this case, thread1 tries to acquire lock1 but within the timeout of 1 second. If it fails to acquire the lock with in a second, then it goes to the else statement and release the lock.

Solution

To prevent the deadlock while using Monitor.TryEnter, we need to ensure the lock is acquired in the same order. To achieve this, ordering the locks based on hash code.

static void ExecuteThread1()
{
    object firstLock, secondLock;
    if (lock1.GetHashCode() < lock2.GetHashCode())
    {
        firstLock = lock1;
        secondLock = lock2;
    }
    else
    {
        firstLock = lock2;
        secondLock = lock1;
    }

    if (Monitor.TryEnter(firstLock, TimeSpan.FromSeconds(1)))
    {
        try
        {
            Console.WriteLine("Thread 1: Holding lock1...");
            Thread.Sleep(100);

            if (Monitor.TryEnter(secondLock, TimeSpan.FromSeconds(1)))
            {
                try
                {
                    Console.WriteLine("Thread 1: Acquired lock2!");
                }
                finally
                {
                    Monitor.Exit(secondLock);
                }
            }
            else
            {
                Console.WriteLine("Thread 1: Failed to acquire lock2 within 1 second.");
            }
        }
        finally
        {
            Monitor.Exit(firstLock);
        }
    }
    else
    {
        Console.WriteLine("Thread 1: Failed to acquire lock1 within 1 second.");
    }
}

static void ExecuteThread2()
{
    object firstLock, secondLock;
    if (lock1.GetHashCode() < lock2.GetHashCode())
    {
        firstLock = lock1;
        secondLock = lock2;
    }
    else
    {
        firstLock = lock2;
        secondLock = lock1;
    }

    if (Monitor.TryEnter(firstLock, TimeSpan.FromSeconds(1)))
    {
        try
        {
            Console.WriteLine("Thread 2: Holding lock2...");
            Thread.Sleep(100);

            if (Monitor.TryEnter(secondLock, TimeSpan.FromSeconds(1)))
            {
                try
                {
                    Console.WriteLine("Thread 2: Acquired lock1!");
                }
                finally
                {
                    Monitor.Exit(secondLock);
                }
            }
            else
            {
                Console.WriteLine("Thread 2: Failed to acquire lock1 within 1 second.");
            }
        }
        finally
        {
            Monitor.Exit(firstLock);
        }
    }
    else
    {
        Console.WriteLine("Thread 2: Failed to acquire lock2 within 1 second.");
    }
}

Here, the locking is hashed in order to maintain the same order.

Hope it gives some understanding of the causes of Deadlock and preventive mechanism such as lock hierarchy, avoiding nested locks and using lock timeouts. So, in the multithreaded environment, it will help us to create a robust, deadlock free application. It will lead to smoother execution, enhanced performance and better end user experience.

Happy C’Sharping 🙂

Leave a comment