C# doesn't allow locking on a null value. I suppose I could check whether the value is null or not before I lock it, but because I haven't locked it another thread could come along and make the value null! How can I avoid this race condition?
Lock on a value that is never null, e.g.
Object _lockOnMe = new Object();
Object _iMightBeNull;
public void DoSomeKungFu() {
if (_iMightBeNull == null) {
lock (_lockOnMe) {
if (_iMightBeNull == null) {
_iMightBeNull = ... whatever ...;
}
}
}
}
Also be careful to avoid this interesting race condition with double-checked locking: Memory Model Guarantees in Double-checked Locking
_iMightBeNull
needs to be declared as volatile. Or, preferably, one should just use Lazy<T>
for lazy initialization. –
Orfield _lockOnMe
before accessing _iMightBeNull
you do not need volatile. –
Phenocryst _iMightBeNull
for nullness before locking on it. You need to understand memory models to understand why this is broken. For a quick confirmation, see csharpindepth.com/Articles/General/Singleton.aspx#dcl –
Orfield _iMightBeNull
might be set back to null later on. The worst case scenario is that the first null check erroneously identifies _iMightBeNull
as being null, and then locks on _lockOnMe
. IIRC, locks provide a full memory barrier including flush, which means that anyone modifying _iMightBeNull
inside of the lock will flush it upon exiting, and anyone subsequently entering the lock will read the (now updated) value. –
Symer You cannot lock on a null value because the CLR has no place to attach the SyncBlock to, which is what allows the CLR to synchronize access to arbitrary objects via Monitor.Enter/Exit (which is what lock
uses internally)
Lock on a value that is never null, e.g.
Object _lockOnMe = new Object();
Object _iMightBeNull;
public void DoSomeKungFu() {
if (_iMightBeNull == null) {
lock (_lockOnMe) {
if (_iMightBeNull == null) {
_iMightBeNull = ... whatever ...;
}
}
}
}
Also be careful to avoid this interesting race condition with double-checked locking: Memory Model Guarantees in Double-checked Locking
_iMightBeNull
needs to be declared as volatile. Or, preferably, one should just use Lazy<T>
for lazy initialization. –
Orfield _lockOnMe
before accessing _iMightBeNull
you do not need volatile. –
Phenocryst _iMightBeNull
for nullness before locking on it. You need to understand memory models to understand why this is broken. For a quick confirmation, see csharpindepth.com/Articles/General/Singleton.aspx#dcl –
Orfield _iMightBeNull
might be set back to null later on. The worst case scenario is that the first null check erroneously identifies _iMightBeNull
as being null, and then locks on _lockOnMe
. IIRC, locks provide a full memory barrier including flush, which means that anyone modifying _iMightBeNull
inside of the lock will flush it upon exiting, and anyone subsequently entering the lock will read the (now updated) value. –
Symer There are two issues here:
First, don't lock on a null
object. It doesn't make sense as how can two objects, both null
, be differentiated?
Second, to safely initialise a variable in a multithreaded environment, use the double-checked locking pattern:
if (o == null) {
lock (lockObj) {
if (o == null) {
o = new Object();
}
}
}
This will ensure that another thread has not already initialised the object and can be used to implement the Singleton pattern.
Why doesn't C# allow a null value to be locked?
Paul's answer is the only technically correct one so far so I would accept that one. It is because monitors in .NET use the sync block which is attached to all reference types. If you have a variable that is null
then it is not referring to any object and that means the monitor does not have access to a usable sync block.
How can I avoid this race condition?
The traditional approach is to lock on an object reference which you know will never be null
. If you find yourself in a situation where this cannot be guaranteed then I would classify this a non-traditional approach. There really is not much more I can mention here unless you describe in more detail the particular scenario that can lead to nullable lock targets.
First part of your question is already answered but I would like to add something for second part of your question.
It is simpler to use a different object to perform the locking, specially in this condition. This also resolve the issue to maintain the states of multiple shared objects in a critical section, e.g. list of employee and list of employee photos.
Moreover this technique is also useful when you have to acquire lock on primitive types e.g int or decimal etc.
In my opinion if you are use this technique as everybody else suggested then you don't need to perform null check twice. e.g. in accepted answer Cris has used if condition twice which really doesn't make any difference because the locked object is different then what is actually being modified, if you are locking on a different object then performing the first null check is useless and waste of cpu.
I would suggest the following piece of code;
object readonly syncRootEmployee = new object();
List<Employee> employeeList = null;
List<EmployeePhoto> employeePhotoList = null;
public void AddEmployee(Employee employee, List<EmployeePhoto> photos)
{
lock (syncRootEmployee)
{
if (employeeList == null)
{
employeeList = new List<Employee>();
}
if (employeePhotoList == null)
{
employeePhotoList = new List<EmployeePhoto>();
}
employeeList.Add(employee);
foreach(EmployeePhoto ep in photos)
{
employeePhotoList.Add(ep);
}
}
}
I can't see any race condition in here if anybody else see race condition please do respond in comments. As you can see in above code it resolve 3 problem at once, one no null check required before locking, second it creates a critical section without locking two shared sources, and third locking more than one object cause deadlocks due to lack of attention while writing code.
Following is how I use the locks on primitive types.
object readonly syncRootIteration = new object();
long iterationCount = 0;
long iterationTimeMs = 0;
public void IncrementIterationCount(long timeTook)
{
lock (syncRootIteration)
{
iterationCount++;
iterationTimeMs = timeTook;
}
}
public long GetIterationAvgTimeMs()
{
long result = 0;
//if read without lock the result might not be accurate
lock (syncRootIteration)
{
if (this.iterationCount > 0)
{
result = this.iterationTimeMs / this.iterationCount;
}
}
return result;
}
Happy threading :)
There's a much simpler answer here that no one else has highlighted - design choice. Clearly they could have made lock (null)
a no-op the same way using (null)
is. They didn't.
There's a championed issue in the dotnet Github repo about await null
that provides some insight on the thinking of TPTB here. When asked why allow using (null)
but not await null
, one of the contributors made his feelings clear:
Legacy. Doing it again, i would absolutely not do that.
It's not an uncommon view; many in the .NET world don't like the idea of automatically saving us from NullReferenceException
, even when it would be convenient to do so.
Case in point, I have a method that may or may not need to be locked in synchronization with other threads, so I pass in lockObj
as a parameter. If locking isn't needed, lockObj
is null
.
It would be nice and clean to just say:
lock (lockObj)
{
...
}
and not have to worry about this. Instead I'm given two rather unpleasant choices:
Option A
lock (lockObj ?? new object())
{
...
}
While this seems innocuous, it is still forcing a thread lock needlessly. This is not without cost.
Option B
void Action()
{
...
}
if (lockObj != null)
lock(lockObj) action();
else
action();
Despite being ugly it's the better of two bad choices.
But there's a big and probably winning counter-argument to allowing lock (null)
to behave like using (null)
- if the lock object is null
due to a mistake, you'd never know it. The code would run in a non-threadsafe manner and you'd get thread racing errors which are even more difficult to diagnose.
In sum, "why" something is allowed or not allowed in .NET is virtually always a design choice. A lot of thought and discussion usually goes into those choices. Anyone interested should follow the dotnet
issues on Github to see just how much.
© 2022 - 2024 — McMap. All rights reserved.