For two decades, the pattern has been the same: declare a private readonly object _lock = new(), wrap your critical section in lock(_lock), and hope nobody accidentally locks on this, a string literal, or some other shared reference. It works, but it’s a pattern held together by convention, not by the type system.
.NET 9 introduces System.Threading.Lock, a purpose-built type that the compiler recognizes and optimizes. No more locking on arbitrary objects.
The Old Way
public class ConnectionPool{ private readonly object _lock = new(); private readonly List<Connection> _connections = [];
public Connection Acquire() { lock (_lock) { var conn = _connections.FirstOrDefault(c => !c.InUse); if (conn is not null) conn.InUse = true; return conn ?? CreateNew(); } }}The compiler turns that lock into Monitor.Enter / Monitor.Exit. It works, but there’s no type safety — you could accidentally pass _lock to something that locks on it elsewhere, or some junior dev writes lock(this) in a code review at 5 PM.
The New Way
using System.Threading;
public class ConnectionPool{ private readonly Lock _lock = new(); private readonly List<Connection> _connections = [];
public Connection Acquire() { lock (_lock) { var conn = _connections.FirstOrDefault(c => !c.InUse); if (conn is not null) conn.InUse = true; return conn ?? CreateNew(); } }}Looks almost identical, right? The difference is under the hood. When the compiler sees lock on a System.Threading.Lock, it emits a call to Lock.EnterScope() which returns a ref struct that releases the lock on Dispose. This is more efficient than Monitor.Enter/Exit because the JIT can optimize the scope-based pattern better.
Scoped Locking
You can also use the scope explicitly when you need more control:
public void TransferAll(ConnectionPool other){ using (_lock.EnterScope()) { foreach (var conn in _connections) { other.Add(conn); } _connections.Clear(); }}The Lock.Scope is a ref struct, so it can’t escape the method or get boxed. The lock is guaranteed to release when the scope ends, even if an exception fires.
Why Bother?
- Type safety. You can’t accidentally lock on a string, a type, or
this. The type system prevents misuse. - Performance.
EnterScope()avoids the overhead ofMonitor’s thread-affinity tracking in the uncontended case. - Intent. Seeing
Lockin a field declaration screams “synchronization primitive.” Seeingobjectscreams nothing.
Key Takeaway
Replace private readonly object _lock = new() with private readonly Lock _lock = new(). Same lock keyword, same syntax, better codegen, and the type system finally prevents the classic locking mistakes. One find-and-replace, zero behavior changes.