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

谁都会走 提交于 2019-12-01 15:34:58
Chris Shain

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

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)

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.

Brian Gideon

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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!