How do you avoid deadlocks with async/await in C#?
Quick answer
Use async all the way down instead of blocking on tasks with .Result or .Wait(), and in library code use ConfigureAwait(false) so continuations do not need to return to a captured context.
The classic deadlock happens in UI apps and legacy ASP.NET: the calling thread blocks on task.Result, while the awaited method is waiting to resume on that same captured synchronisation context. Neither can proceed. ASP.NET Core has no such context, so it avoids this particular deadlock, but blocking calls still waste threads and can cause thread-pool starvation under load.
Rules to follow: make the whole call chain async and await it; never call .Result, .Wait() or .GetAwaiter().GetResult() on a task that is not already complete; avoid async void except for event handlers; pass a CancellationToken through your calls; and use ConfigureAwait(false) in general-purpose libraries. Lock statements cannot contain await, so use SemaphoreSlim.WaitAsync() for async mutual exclusion.
// risky: blocks the calling thread
var data = GetDataAsync().Result;
// correct: async all the way
public async Task<IActionResult> Index(CancellationToken ct)
{
var data = await GetDataAsync(ct);
return Ok(data);
}Key points
- Do not block on async code
- Avoid async void
- ConfigureAwait(false) in libraries, CancellationToken everywhere