C# lock, Monitor and deadlocks: why you can't await inside a lock, and what to use instead

C# Async & Multithreading November 13, 2026 csharp

lock (obj) = Monitor.Enter / Exit inside try/finally.

csharp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ======================================================================
// Q: How do lock and deadlocks work? Why can't you await inside a lock?
//
// - lock (obj) = Monitor.Enter / Exit inside try/finally. In .NET 9+ you write
//   private readonly Lock _lock = new();, which is faster.
// - await inside a lock: compile error. The lock belongs to a thread, and the
//   continuation may run on a different thread. For an async critical section:
//   SemaphoreSlim(1, 1) (Q89).
// - Don't lock on this, typeof(X) or a string: other code may lock on the same
//   object. Always lock on a private readonly field.
// - Deadlock: thread A holds L1 and waits for L2, while B holds L2 and waits for
//   L1. Prevention: a consistent lock order, small critical sections, no
//   external calls (HTTP, database, callbacks) inside a lock, timeouts
//   (Monitor.TryEnter, WaitAsync(timeout)).
// - Many readers, rare changes: instead of a lock, an immutable snapshot that is
//   replaced as a whole, as shown in the code.
// ======================================================================

// Immutable FrozenDictionary snapshot instead of a lock
private volatile FrozenDictionary<string, decimal> _prices = FrozenDictionary<string, decimal>.Empty;

// Reload: build a new dictionary and swap the reference (atomic). Readers never lock.
public void Reload(IEnumerable<PriceRow> rows) => _prices = rows.ToFrozenDictionary(r => r.Sku, r => r.Amount);
public decimal? Get(string sku) => _prices.TryGetValue(sku, out var p) ? p : null;