Locks, Monitor, and their costs
Monitor internals, lock contention, ReaderWriterLockSlim, SemaphoreSlim โ and when a lock is the wrong tool.
Locks, Monitor, and their costs
lock (obj) compiles to Monitor.Enter/Monitor.Exit in a try/finally.
The monitor lives in the object header โ no allocation, but every object
can be a lock, which is exactly why locking on this, on a Type, or on a
string is dangerous (anything else might lock the same object: deadlock by
stranger). Private final lock objects are the discipline.
private readonly object _gate = new(); // never lock(this), lock(Type), or a string
lock (_gate) { /* invariant region */ }
Contention is the tax: at zero contention a lock is a couple of interlocked instructions; under contention threads park and unpark โ microseconds become milliseconds, and context switches burn CPU. The escalation ladder, cheapest first:
- Do nothing shared. Immutable data needs no locking at all.
Interlockedfor single-variable updates โ no parking.SemaphoreSlimwhen the critical section awaits (alockcannot containawaitโ the compiler forbids it, because a parked thread must not hold a thread-affine lock across suspension).ReaderWriterLockSlimfor read-mostly structures โ many readers or one writer. Note its own overhead: for tiny critical sections it is slower than a plain lock.- Lock-free data structures โ only from well-tested primitives
(concurrent collections); hand-rolling with
CompareExchangeloops is where ABA bugs are born.
The deadlock recipe is always the same: two locks, two threads, opposite
orders. Fix = global lock ordering (by id, as the checkpoint does) or
TryEnter with timeout + rollback. Livelock: threads keep retrying and
starving each other โ backoff (Thread.Sleep with jitter) is the cure.