Skip to main content
๐Ÿ“œ WAYPOINT LESSON

Locks, Monitor, and their costs

โญโญโญ advancedโณ 15 min read๐Ÿ“ Lesson 198 of 265

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:

  1. Do nothing shared. Immutable data needs no locking at all.
  2. Interlocked for single-variable updates โ€” no parking.
  3. SemaphoreSlim when the critical section awaits (a lock cannot contain await โ€” the compiler forbids it, because a parked thread must not hold a thread-affine lock across suspension).
  4. ReaderWriterLockSlim for read-mostly structures โ€” many readers or one writer. Note its own overhead: for tiny critical sections it is slower than a plain lock.
  5. Lock-free data structures โ€” only from well-tested primitives (concurrent collections); hand-rolling with CompareExchange loops 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.