Deadlocks

1 min read

In a deadlock, threads wait on each other forever. None can continue, and none uses CPU. Four conditions must all hold: mutual exclusion (a resource has one holder), hold and wait (a thread holds one resource while waiting for another), no preemption (resources are released only voluntarily), and circular wait (each thread waits for one held by the next). Removing any one prevents it.

Locks taken in opposite order

Thread A holds lock 1 and waits for lock 2. Thread B holds lock 2 and waits for lock 1. Neither can continue. Thread A needs lock 2 Thread B needs lock 1 Lock 1 held by A Lock 2 held by B holds holds waits for waits for
Circular wait.
C#
// Thread 1                      // Thread 2
lock (a)                         lock (b)
{                                {
    lock (b) { /* ... */ }           lock (a) { /* ... */ }
}                                }

Avoiding it

Take locks in the same order everywhere, a lock hierarchy, which removes the circular wait. Where an order is hard to enforce, Monitor.TryEnter with a timeout lets a thread give up and back off. Fewer locks, held briefly and without calling unknown code, make it less likely.

The async deadlock

Calling .Result or .Wait() on a task deadlocks where a SynchronizationContext exists, as in UI apps and classic ASP.NET. The blocked thread holds the context, and the method’s continuation needs that same context to resume.

C#
// On a UI thread:
string text = GetTextAsync().Result; // blocks the UI thread

async Task<string> GetTextAsync()
{
    await Task.Delay(100);           // the continuation needs the UI thread back
    return "done";
}

The fix is to await all the way up instead of blocking. ConfigureAwait(false) stops a continuation needing the context, but only if every await in the chain uses it (see async pitfallsAsync pitfallsFour things to watch for when writing async code.Opens in a new tab). ASP.NET Core and console apps have no SynchronizationContext, so this exact deadlock does not happen there, but blocking still ties up a pool thread (see thread pool starvationLivelock and starvationThreads that keep running without progress, and threads that never get a turn.Opens in a new tab).

Finding one

A hung app with threads parked on locks or .Result shows it in a dump: Visual Studio’s Parallel Stacks window, or dotnet-dump, lists what each thread is waiting on.


Sources