Do Not Lock This
ID |
csharp.do_not_lock_this |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
CSharp |
Tags |
concurrency, reliability |
Description
Reports lock statements whose target cannot provide reliable mutual exclusion. Three
families of bad target are covered: objects that are publicly reachable, targets whose
identity varies per call, and targets whose value can be replaced while the lock is held.
Rationale
A lock is only useful when the locked object is private to the synchronisation, stable
for the lifetime of the guarded state, and shared by exactly the threads that must
exclude one another. Each family below breaks one of those conditions.
Publicly reachable. When the locked object is this, any external caller that happens
to call lock(myObj) blocks on the same monitor, so code outside the type can deadlock
it. typeof(SomeClass) returns a globally shared Type instance, and strings are
interned by the runtime — two unrelated files containing lock("MyKey") lock the same
object. A readonly string field is no better: readonly fixes the reference, while the
instance it points at remains interned and shared.
Identity varies per call. A local variable or a parameter is evaluated afresh on every invocation, so two threads running the same method lock two different objects and neither excludes the other. This is synchronisation that compiles, runs, and protects nothing — the most dangerous variety, because the code looks correct.
The exception is a local or parameter captured by a lambda, anonymous method or local
function that contains the lock: the captured variable lives in the closure created once,
shared by every invocation of that delegate, so its identity is stable and the lock is
correct. The rule tells the two apart by comparing the variable’s declaring method/lambda
with the nearest enclosing method/lambda of the lock statement — a mismatch means the
lock runs inside a closure that captured the variable from an outer scope, so no finding is
reported.
Value can be replaced. A writable field may be reassigned while one thread holds the monitor. Later threads then lock the new instance and enter the critical section concurrently with the thread still holding the old one.
The recommended pattern is a private readonly object field used only for locking.
public class Cache
{
private readonly object sync = new object();
private object pending = new object();
public void Bad()
{
lock (this) // FLAW — external callers can also lock(this)
{
// ...
}
}
public void BadType()
{
lock (typeof(Cache)) // FLAW — Type objects are shared globally
{
// ...
}
}
public void BadString()
{
lock ("globalKey") // FLAW — interned string is shared across the process
{
// ...
}
}
public void BadLocal()
{
var gate = new object();
lock (gate) // FLAW — every call locks a different object
{
// ...
}
}
public void BadWritableField()
{
lock (pending) // FLAW — the field can be reassigned mid-lock
{
// ...
}
}
public void Good()
{
lock (sync) // OK — private dedicated lock object
{
// ...
}
}
public Action GoodCapturedLocal()
{
var gate = new object();
Action locker = () =>
{
lock (gate) // OK — gate is captured by the lambda, so every call
{ // through "locker" shares the same instance
// ...
}
};
return locker;
}
}
Remediation
Introduce a private readonly object field that is used only for synchronisation and
lock on that field. Never expose the lock object to external code.
If the lock target is currently a local or a parameter, promote it to such a field: the
whole point is that all threads needing exclusion observe the same instance. If it is
an existing field, add readonly — and if that field is a string, replace it with a
dedicated object rather than relying on readonly, because interning still shares the
instance process-wide.