C# Deadlock

Summary: in this tutorial, you’ll learn about the C# deadlock, how it occurs, and how to fix it.

Introduction to the C# deadlock

A deadlock happens when two or more threads are blocking and waiting for each other to release the locks that they need to proceed.

Particularly, each thread is holding onto a lock that the other thread needs, and neither can proceed until it can acquire the lock it’s waiting for.

For example, suppose you have two threads t1 and t2. Each thread holds a lock on a different shared resource r1 and r2.

Thread t1 needs to access r2 to proceed while thread t2 needs to access r1. Both threads attempt to acquire the resources at the same time. They’re waiting and blocking each other. As a result, a deadlock occurs.

The following program demonstrates how a deadlock scenario occurs when two threads complete multiple locks:

using static System.Console;


var lock1 = new object();
var lock2 = new object();

void AcquireLocks1()
{
    var threadId = Thread.CurrentThread.ManagedThreadId;

    lock (lock1)
    {
        WriteLine($"Thread {threadId} acquired lock 1.");
        Thread.Sleep(1000);
        WriteLine($"Thread {threadId} attempted to acquire lock 2.");
        lock (lock2)
        {
            WriteLine($"Thread {threadId} acquired lock 2.");
        }
    }
}

void AcquireLocks2()
{
    var threadId = Thread.CurrentThread.ManagedThreadId;

    lock (lock2)
    {
        WriteLine($"Thread {threadId} acquired lock 2.");
        Thread.Sleep(1000);
        WriteLine($"Thread {threadId} attempted to acquire lock 1.");
        lock (lock1)
        {
            WriteLine($"Thread {threadId} acquired lock 1.");
        }
    }
}

// Create two new threads that compete for the locks
var thread1 = new Thread(AcquireLocks1);
var thread2 = new Thread(AcquireLocks2);

// Start the threads
thread1.Start();
thread2.Start();

// Wait for the threads to complete
thread1.Join();
thread2.Join();

WriteLine("Finished.");
ReadLine();Code language: C# (cs)

How it works.

First, create two lock objects lock1 and lock2:

var lock1 = new object();
var lock2 = new object();Code language: C# (cs)

Second, define the AcquireLock1 method that acquires lock1, delays for one second, and attempts to acquire lock2:

void AcquireLocks1()
{
    var threadId = Thread.CurrentThread.ManagedThreadId;

    lock (lock1)
    {
        WriteLine($"Thread {threadId} acquired lock 1.");
        Thread.Sleep(1000);
        WriteLine($"Thread {threadId} attempted to acquire lock 2.");
        lock (lock2)
        {
            WriteLine($"Thread {threadId} acquired lock 2.");
        }
    }
}Code language: C# (cs)

Third, define AcquireLock2() method that acquires lock2, delays for one second, and attempts to acquire lock1:

void AcquireLocks2()
{
    var threadId = Thread.CurrentThread.ManagedThreadId;

    lock (lock2)
    {
        WriteLine($"Thread {threadId} acquired lock 2.");
        Thread.Sleep(1000);
        WriteLine($"Thread {threadId} attempted to acquire lock 1.");
        lock (lock1)
        {
            WriteLine($"Thread {threadId} acquired lock 1.");
        }
    }
}Code language: C# (cs)

Fourth, create two threads, start them, and wait for them to complete:

// Create two new threads that compete for the locks
var thread1 = new Thread(AcquireLocks1);
var thread2 = new Thread(AcquireLocks2);

// Start the threads
thread1.Start();
thread2.Start();

// Wait for the threads to complete
thread1.Join();
thread2.Join();Code language: C# (cs)

Output:

Thread 10 acquired lock 1.
Thread 11 acquired lock 2.
Thread 10 attempted to acquire lock 2.
Thread 11 attempted to acquire lock 1.Code language: C# (cs)

Thread1 executes the AcquireLock1 method while thread2 executes the AcquireLock2 method.

Because thread1 is holding the lock1, thread2 is blocked from acquiring the lock1. So thread2 is waiting for thread1 to release the lock1.

Meanwhile, thread2 is holding the lock2, and thread1 is blocked from acquiring the lock1. The thread1 is waiting for thread2 to release the lock2.

Both threads are waiting for each other to get the corresponding locks before proceeding next, which results in a deadlock.

As the result, the program will hang indefinitely, and you’ll never see the “Finished” message on the console.

Fixing a deadlock

To fix a deadlock, you can use one of the following techniques:

  • Avoid holding a lock for too long: when a thread holds a lock for too long, it may cause a deadlock. To avoid it, you need to make sure that you release the lock as soon as you don’t need it.
  • Avoid nested locks: It’s difficult to manage nested locks. And one of the common causes of a deadlock is using nested locks. If you have to use multiple locks, try to acquire them in a fixed order to avoid deadlocks.
  • Use a timeout: the timeout can help prevent a deadlock from occurring. If a thread is waiting for a lock for a certain time that exceeds the timeout, you can throw an exception or take other actions.
  • Use asynchronous programming technique: asynchronous programming allows multiple threads to run concurrently without blocking each other, which helps avoid deadlocks.

The following program demonstrates how to use a timeout to fix a deadlock in the above program:

using static System.Console; 

var lock1 = new object();
var lock2 = new object();

void AcquireLocks1()
{
    var threadId = Thread.CurrentThread.ManagedThreadId;

    while (true)
    {
        if (Monitor.TryEnter(lock1, TimeSpan.FromSeconds(1)))
        {
            try
            {
                WriteLine($"Thread {threadId} acquired lock 1.");
                Thread.Sleep(1000);
                WriteLine($"Thread {threadId} attempted to acquire lock 2.");

                if (Monitor.TryEnter(lock2, TimeSpan.FromSeconds(1)))
                {
                    try
                    {
                        WriteLine($"Thread {threadId} acquired lock 2.");
                        break; // exit the loop if both locks are acquired
                    }
                    finally
                    {
                        Monitor.Exit(lock2);
                    }
                }
            }
            finally
            {
                Monitor.Exit(lock1);
            }
        }
    }

    WriteLine($"Thread {threadId} is done.");
}

void AcquireLocks2()
{
    var threadId = Thread.CurrentThread.ManagedThreadId;

    while (true)
    {
        if (Monitor.TryEnter(lock2, TimeSpan.FromSeconds(1)))
        {
            try
            {
                WriteLine($"Thread {threadId} acquired lock 2.");
                Thread.Sleep(1000);
                WriteLine($"Thread {threadId} attempted to acquire lock 1.");

                if (Monitor.TryEnter(lock1, TimeSpan.FromSeconds(1)))
                {
                    try
                    {
                        WriteLine($"Thread {threadId} acquired lock 1.");
                        break; // exit the loop if both locks are acquired
                    }
                    finally
                    {
                        Monitor.Exit(lock1);
                    }
                }
            }
            finally
            {
                Monitor.Exit(lock2);
            }
        }
    }

    WriteLine($"Thread {threadId} is done.");
}

// Create two new threads that compete for the locks
var thread1 = new Thread(AcquireLocks1);
var thread2 = new Thread(AcquireLocks2);

// Start the threads
thread1.Start();
thread2.Start();

// Wait for the threads to complete
thread1.Join();
thread2.Join();

WriteLine("Finished.");
ReadLine();Code language: C# (cs)

In this program, we use the Monitor.TryEnter() to attempt to acquire a lock with a timeout of one second.

If a thread cannot acquire the lock within one second, it releases the lock and tries again.

The while loop keeps trying until both threads can acquire the locks, which breaks the loop and the threads complete their executions.

Summary

  • A deadlock occurs when two or more threads are waiting and blocking each other to release locks.
Was this tutorial helpful ?