Deadlock in C#

Deadlock in C# with Example

In this article, I am going to discuss Deadlock in C# with Examples. Please read our previous article where we discussed SemaphoreSlim in C# with Examples. Deadlock is one of the most important aspects to understand as a developer. As part of this article, we are going to discuss the following pointers.

  1. What is deadlock?
  2. Why did a Deadlock occur?
  3. How a deadlock can occur in a multithreaded application?
  4. How to avoid a Deadlock by using Monitor.TryEnter method?
  5. How to avoid Deadlock by acquiring locks in a specific order?
What is a Deadlock in C#?

In simple words, we can define a deadlock in C# as a situation where two or more threads are unmoving or frozen in their execution because they are waiting for each other to finish.

For example, let’s say we have two threads Thread1 and Thread2 and at the same time let’s say we have two resources Resource1 and Resource2. The Thread1 locked the Resource1 and tried to acquire a lock on Respurce2. At the same time, Thread2 acquired a lock on Resource2 and tried to acquire a lock on Resource1.

Deadlock in C# in Multithread application

As you can see in the above image, Thread1 is waiting to acquire a lock on Resource2 which is held by Thread2. Thread2 also can’t finish its work and release the lock on Resource2 because it is waiting to acquire a lock on Resource1 which is locked by Thread1, and hence a Deadlock situation occurred.

Deadlock can occur if the following conditions hold true:

  1. Mutual Exclusion: This implies that only one thread can have access to a resource at a particular time.
  2. Hold and Wait: This is a condition in which a thread is holding at least one resource and waiting for a minimum of one resource already acquired by another thread.
  3. No Pre-emption: If a thread has acquired a resource, it cannot be taken away from the thread until it relinquishes control of the resource voluntarily.
  4. Circular Wait: This is a condition in which two or more threads are waiting for a resource acquired by the next member in the chain.
Example to understand Deadlock in C#:

Let us understand Deadlock in C# with an example. Create a class file with the name Account.cs and then copy and paste the following code into it.

namespace DeadLockDemo
{
    public class Account
    {
        public int ID { get; }
        private double Balance { get; set;}

        public Account(int id, double balance)
        {
            ID = id;
            Balance = balance;
        }
        
        public void WithdrawMoney(double amount)
        {
            Balance -= amount;
        }

        public void DepositMoney(double amount)
        {
            Balance += amount;
        }
    }
}

The above Account class is very straightforward. We created the class with two properties i.e. ID and Balance. Through the constructor of this class, we are initializing these properties. So, at the time of Account class instance creation, we need to pass the ID and Balance value. Here we have also created two methods. The WithdrawMoney method is used for withdrawing the amount while the DepositMoney method is used for adding the amount.

AccountManager.cs:

Next, create another class file with the name AccountManager.cs and then copy and paste the following code into it.

using System;
using System.Threading;

namespace DeadLockDemo
{
    public class AccountManager
    {
       private Account FromAccount;
       private Account ToAccount;
       private double TransferAmount;

        public AccountManager(Account AccountFrom, Account AccountTo, double AmountTransfer)
        {
            FromAccount = AccountFrom;
            ToAccount = AccountTo;
            TransferAmount = AmountTransfer;
        }

        public void FundTransfer()
        {
            Console.WriteLine($"{Thread.CurrentThread.Name} trying to acquire lock on {FromAccount.ID}");
            lock (FromAccount)
            {
                Console.WriteLine($"{Thread.CurrentThread.Name} acquired lock on {FromAccount.ID}");
                Console.WriteLine($"{Thread.CurrentThread.Name} Doing Some work");
                Thread.Sleep(1000);
                Console.WriteLine($"{Thread.CurrentThread.Name} trying to acquire lock on {ToAccount.ID}");

                lock (ToAccount)
                {
                    FromAccount.WithdrawMoney(TransferAmount);
                    ToAccount.DepositMoney(TransferAmount);
                }
            }
        }
    }
}

In the above code, we created two Account type variables to hold the FromAccount and ToAccount details i.e. the Account from where the amount is going to be deducted and the account to whom the amount is created. We also created another double-type variable i.e. TransferAmount to hold the amount which is going to be deducted from the FromAccount and credited to the ToAccount. Through the constructor of this class, we are initializing these variables.

We also created the FundTransfer method which is going to perform the required task. As you can see, it first acquires a lock on From Account and then does some work. After 1 second it backs and tries to acquire a lock on To Account.

Modifying the Main Method:

Now modify the Main method of the Program class as shown below. Here, for accountManager1, Account1001 is the FromAccount and Account1002 is the ToAccount. Similarly, for accountManager2, Account1002 is the FromAccount and Account1001 is the ToAccount

using System;
using System.Threading;

namespace DeadLockDemo
{
    class Program
    {
        public static void Main()
        {
            Console.WriteLine("Main Thread Started");
            Account Account1001 = new Account(1001, 5000);
            Account Account1002 = new Account(1002, 3000);

            AccountManager accountManager1 = new AccountManager(Account1001, Account1002, 5000);
            Thread thread1 = new Thread(accountManager1.FundTransfer)
            {
                Name = "Thread1"
            };

            AccountManager accountManager2 = new AccountManager(Account1002, Account1001, 6000);
            Thread thread2 = new Thread(accountManager2.FundTransfer)
            {
                Name = "Thread2"
            };

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

            thread1.Join();
            thread2.Join();
            Console.WriteLine("Main Thread Completed");
            Console.ReadKey();
        }
    }
}
Output:

Deadlock in C#

Note: For thread1, Account1001 is resource1 and Account1002 is resource2. On the other hand, for thread2, Account1002 is resource1 and Account1001 is resource2. With this keep in mind run the application and see deadlock occurred.

The reason for the deadlock is that thread1 acquired an exclusive lock on Account1001 and then do some processing. Meantime thread2 started and it acquired an exclusive lock on Account1002 and then does some processing. Then thread1 back and wants to acquire a lock on Account1001 which is already locked by thread2. Similarly, thread2 is back and wants to acquire a lock on Account1002 which is already locked by thread1 and hence deadlock.

Avoiding Deadlock by using Monitor.TryEnter method?

One of the overloaded versions (TryEnter(object obj, int millisecondsTimeout)) of the Monitor.TryEnter method takes the second parameter as the time out in milliseconds. Using that parameter, we can specify a timeout for the thread to release the lock. If a thread is holding a resource for a long time while the other thread is waiting, then Monitor will provide a time limit and force the lock to release it. So that the other thread will enter into the critical section. Modifying the AccountManager class as shown below:

using System;
using System.Threading;

namespace DeadLockDemo
{
    public class AccountManager
    {
       private Account FromAccount;
       private Account ToAccount;
       private double TransferAmount;

        public AccountManager(Account AccountFrom, Account AccountTo, double AmountTransfer)
        {
            this.FromAccount = AccountFrom;
            this.ToAccount = AccountTo;
            this.TransferAmount = AmountTransfer;
        }

        public void FundTransfer()
        {
            Console.WriteLine($"{Thread.CurrentThread.Name} trying to acquire lock on {FromAccount.ID}");
            
            lock (FromAccount)
            {
                Console.WriteLine($"{Thread.CurrentThread.Name} acquired lock on {FromAccount.ID}");
                Console.WriteLine($"{Thread.CurrentThread.Name} Doing Some work");
                Thread.Sleep(3000);
                Console.WriteLine($"{Thread.CurrentThread.Name} trying to acquire lock on {ToAccount.ID}");
                
                if (Monitor.TryEnter(ToAccount, 3000))
                {
                    Console.WriteLine($"{Thread.CurrentThread.Name} acquired lock on {ToAccount.ID}");
                    try
                    {
                        FromAccount.WithdrawMoney(TransferAmount);
                        ToAccount.DepositMoney(TransferAmount);
                    }
                    finally
                    {
                        Monitor.Exit(ToAccount);
                    }
                }
                else
                {
                    Console.WriteLine($"{Thread.CurrentThread.Name} Unable to acquire lock on {ToAccount.ID}, So existing.");
                }
            }
        }
    }
}
Output:

How to avoid Deadlock in C# by using Monitor.TryEnter method

As you can see in the output thread1 release the lock and exit from the critical section which allows thread2 to enter the critical section and complete its task.

How do Avoid Deadlock in C# by acquiring locks in a specific order?

Please modify the AccountManager class as shown below.

using System;
using System.Threading;

namespace DeadLockDemo
{
    public class AccountManager
    {
       private Account FromAccount;
       private Account ToAccount;
       private readonly double TransferAmount;
       private static readonly Mutex mutex = new Mutex();

        public AccountManager(Account AccountFrom, Account AccountTo, double AmountTransfer)
        {
            this.FromAccount = AccountFrom;
            this.ToAccount = AccountTo;
            this.TransferAmount = AmountTransfer;
        }

        public void FundTransfer()
        {
            object _lock1, _lock2;

            if (FromAccount.ID < ToAccount.ID)
            {
                _lock1 = FromAccount;
                _lock2 = ToAccount;
            }
            else
            {
                _lock1 = ToAccount;
                _lock2 = FromAccount;
            }

            Console.WriteLine($"{Thread.CurrentThread.Name} trying to acquire lock on {((Account)_lock1).ID}");
            
            lock (_lock1)
            {
                Console.WriteLine($"{Thread.CurrentThread.Name} acquired lock on {((Account)_lock1).ID}");
                Console.WriteLine($"{Thread.CurrentThread.Name} Doing Some work");
                Thread.Sleep(3000);
                Console.WriteLine($"{Thread.CurrentThread.Name} trying to acquire lock on {((Account)_lock2).ID}");
                lock(_lock2)
                {
                    Console.WriteLine($"{Thread.CurrentThread.Name} acquired lock on {((Account)_lock2).ID}");
                    FromAccount.WithdrawMoney(TransferAmount);
                    ToAccount.DepositMoney(TransferAmount);
                }
            }
        }
    }
}
Output:

How to avoid Deadlock in C# by acquiring locks in a specific order

In the next article, I am going to show you the Performance of a multithreaded program when running on a single-core/processor machine versus a multi-core/processor machine. Here, in this article, I try to explain Deadlock in C# with different approaches. I hope you enjoy this Deadlock in C# with Examples article.

2 thoughts on “Deadlock in C#”

  1. blank

    One thing, in “Avoiding Deadlock by using Monitor.TryEnter method?”
    Shouldn’t you use lock object in instead of FromAccount?
    So:
    private object threadLock = new object();

    lock(threadLock)

    insted of
    lock (FromAccount)

Leave a Reply

Your email address will not be published. Required fields are marked *