0
0
CsharpDebug / FixIntermediate · 4 min read

How to Avoid Deadlock in C#: Simple Fixes and Best Practices

To avoid deadlock in C#, always acquire locks in a consistent order and avoid blocking calls inside locks. When using async methods, use await instead of blocking with .Result or .Wait() to prevent thread blocking that causes deadlocks.
🔍

Why This Happens

Deadlock happens when two or more threads wait forever for each other to release resources. In C#, this often occurs when locks are taken in different orders or when blocking calls are made inside locks, especially with async code blocking on .Result or .Wait().

csharp
using System;
using System.Threading;
using System.Threading.Tasks;

class DeadlockExample
{
    private static readonly object lockA = new object();
    private static readonly object lockB = new object();

    public static void Main()
    {
        var t1 = Task.Run(() => {
            lock (lockA)
            {
                Thread.Sleep(100);
                lock (lockB)
                {
                    Console.WriteLine("Task 1 acquired both locks");
                }
            }
        });

        var t2 = Task.Run(() => {
            lock (lockB)
            {
                Thread.Sleep(100);
                lock (lockA)
                {
                    Console.WriteLine("Task 2 acquired both locks");
                }
            }
        });

        Task.WaitAll(t1, t2);
    }
}
Output
(Program hangs indefinitely with no output due to deadlock)
🔧

The Fix

To fix deadlock, always acquire locks in the same order in all threads. This prevents circular waiting. Also, avoid blocking on async methods by using await instead of .Result or .Wait().

csharp
using System;
using System.Threading;
using System.Threading.Tasks;

class FixedDeadlockExample
{
    private static readonly object lockA = new object();
    private static readonly object lockB = new object();

    public static void Main()
    {
        var t1 = Task.Run(() => {
            lock (lockA)
            {
                Thread.Sleep(100);
                lock (lockB)
                {
                    Console.WriteLine("Task 1 acquired both locks");
                }
            }
        });

        var t2 = Task.Run(() => {
            lock (lockA)  // Changed order to match t1
            {
                Thread.Sleep(100);
                lock (lockB)
                {
                    Console.WriteLine("Task 2 acquired both locks");
                }
            }
        });

        Task.WaitAll(t1, t2);
    }
}
Output
Task 1 acquired both locks Task 2 acquired both locks
🛡️

Prevention

To avoid deadlocks in the future, follow these best practices:

  • Always lock multiple resources in a consistent global order.
  • Keep locked sections short and avoid calling external code inside locks.
  • Prefer async/await over blocking calls like .Result or .Wait().
  • Use timeout or cancellation tokens to detect and recover from potential deadlocks.
  • Consider using higher-level concurrency tools like SemaphoreSlim or Concurrent collections.
⚠️

Related Errors

Other common threading issues include:

  • Race conditions: When threads access shared data without proper synchronization, causing unpredictable results.
  • Thread starvation: When some threads never get CPU time because others hold resources too long.
  • Lock recursion: When a thread tries to acquire a lock it already holds, causing exceptions if not using recursive locks.

Key Takeaways

Always acquire locks in the same order to prevent circular waiting and deadlocks.
Avoid blocking on async code; use await instead of .Result or .Wait().
Keep locked code sections short and avoid calling external or blocking code inside locks.
Use concurrency tools like SemaphoreSlim and cancellation tokens to manage resources safely.
Detect deadlocks early with timeouts and design code to minimize shared resource contention.