Why doesn't C# allow a null value to be locked?
Asked Answered
I

6

43

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?

Incompetence answered 29/8, 2011 at 4:58 Comment(2)
Why don't you just use a member that initialized statically and always is not nullVedette
As I understand it, null is essentially nothing. How can you put a lock on nothing? In other words, string myString = null declares a variable of type string, but that's all there is to it - it doesn't exist as an object because it has no value.Dunston
S
32

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

Symer answered 29/8, 2011 at 5:1 Comment(6)
Might be nice to add readonly to the lock object to call out the desired immutabilityZinc
why locking _lockOnMe can prevent others access _iMightBeNull?Chantry
-1: Your code is still susceptible to the race condition that you linked to; _iMightBeNull needs to be declared as volatile. Or, preferably, one should just use Lazy<T> for lazy initialization.Orfield
@Orfield How so? As long as you always lock _lockOnMe before accessing _iMightBeNull you do not need volatile.Phenocryst
@SourceOverflow: The code above first checks _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#dclOrfield
Volatility is only an issue if _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
E
58

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)

Economy answered 29/8, 2011 at 5:15 Comment(0)
S
32

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

Symer answered 29/8, 2011 at 5:1 Comment(6)
Might be nice to add readonly to the lock object to call out the desired immutabilityZinc
why locking _lockOnMe can prevent others access _iMightBeNull?Chantry
-1: Your code is still susceptible to the race condition that you linked to; _iMightBeNull needs to be declared as volatile. Or, preferably, one should just use Lazy<T> for lazy initialization.Orfield
@Orfield How so? As long as you always lock _lockOnMe before accessing _iMightBeNull you do not need volatile.Phenocryst
@SourceOverflow: The code above first checks _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#dclOrfield
Volatility is only an issue if _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
C
6

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.

Cithara answered 29/8, 2011 at 5:13 Comment(0)
C
2

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.

Carsoncarstensz answered 29/8, 2011 at 13:32 Comment(1)
seems like your link to Paul answer is brokenLanciform
F
2

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 :)

Fatty answered 27/3, 2014 at 14:10 Comment(0)
S
0

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.

Sills answered 17/1 at 17:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.