为什么C#不允许锁定空值?

42

C#不允许在null值上进行锁定。我想我可以在锁定之前检查该值是否为null,但是因为我没有对它进行锁定,另一个线程可能会将该值变为null!我该如何避免这种竞争条件呢?


5
为什么不直接使用一个静态初始化的成员变量,它始终不为空? - zerkms
2
据我的理解,null本质上是什么都没有。你怎么能在空无一物上设置锁呢?换句话说,string myString = null声明了一个字符串类型的变量,但这就是全部 - 它不存在作为对象,因为它没有值。 - Tim
5个回答

58
由于CLR没有地方附加SyncBlock,因此无法在null值上进行锁定,而这正是CLR通过Monitor.Enter/Exit(也是lock内部使用的内容)同步访问任意对象的方式。

32

锁定一个永远不会为null的值,例如:

Object _lockOnMe = new Object();
Object _iMightBeNull;
public void DoSomeKungFu() {
    if (_iMightBeNull == null) {
        lock (_lockOnMe) {
            if (_iMightBeNull == null) {
                _iMightBeNull = ...  whatever ...;
            }
        }
    }
}

同时要小心避免使用双重检查锁定时出现的有趣竞态条件:双重检查锁定中的内存模型保证


8
可以考虑将"lock object"添加"readonly"关键字以明确所需的不可变性。 - cordialgerm
1
为什么锁定_lockOnMe可以防止其他人访问_iMightBeNull? - Sheldon Wei
-1:你的代码仍然容易受到你所提到的竞态条件的影响;_iMightBeNull 需要声明为 volatile。或者更好的做法是,使用 Lazy<T> 进行延迟初始化。 - Douglas
1
@道格拉斯 怎么做呢?只要在访问 _iMightBeNull 之前始终锁定 _lockOnMe,就不需要使用 volatile。 - SourceOverflow
@SourceOverflow:上面的代码首先检查 _iMightBeNull 是否为 null,然后才对其进行锁定。您需要了解内存模型才能理解为什么这是错误的。如果想快速确认,请参见 http://csharpindepth.com/Articles/General/Singleton.aspx#dcl - Douglas
只有当 _iMightBeNull 以后可能被设置为 null 时,不稳定性才是一个问题。最坏的情况是第一个 null 检查错误地将 _iMightBeNull 识别为空,并在 _lockOnMe 上锁定。如果我没记错的话,锁提供了完整的内存屏障,包括刷新,这意味着在锁内修改 _iMightBeNull 的任何人都会在退出时刷新它,并且随后进入锁的任何人都将读取(现在已更新的)值。 - Chris Shain

6

这里有两个问题:

首先,不要对一个null对象进行锁定。这是没有意义的,因为两个都是 null 的对象是无法区分的。

其次,在多线程环境中安全地初始化变量,可以使用双重检查锁定模式:

if (o == null) {
    lock (lockObj) {
        if (o == null) {
            o = new Object();
        }
    }
}

这将确保另一个线程尚未初始化对象并可用于实现单例模式。

2

你的问题的第一部分已经得到了回答,但我想为你的问题的第二部分添加一些内容。

在这种情况下,使用不同的对象来执行锁定更为简单。这也解决了在临界区中维护多个共享对象状态的问题,例如雇员列表和雇员照片列表。

此外,当您必须在原始类型(如int或decimal等)上获取锁时,此技术也非常有用。

我认为,如果您像其他人建议的那样使用此技术,则不需要两次执行空值检查。例如,在接受的答案中,Cris使用了两次if条件语句,这确实没有任何区别,因为锁定的对象与实际修改的对象不同,如果您锁定了一个不同的对象,则执行第一次空值检查是无用而浪费cpu的。

我建议使用以下代码:

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);
        }
    }
}

如果有人发现了竞争条件,请在评论中回复。从上面的代码可以看出,它解决了三个问题:首先,在锁定之前不需要进行空值检查;其次,它创建了一个关键部分而不锁定两个共享源;第三,锁定多个对象会因编写代码时缺乏注意而导致死锁。

以下是我如何使用原始类型的锁。

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;
}

多线程愉快 :)


2
为什么C#不允许锁定null值? Paul's answer 到目前为止是唯一正确的答案,所以我会接受它。这是因为.NET中的监视器使用与所有引用类型附加的同步块。如果您有一个为null的变量,则它不引用任何对象,这意味着监视器无法访问可用的同步块。
如何避免这种竞态条件?
传统方法是锁定您知道永远不会为null的对象引用。如果您发现自己处于无法保证这一点的情况下,那么我会将其归类为非传统方法。除非您描述导致可空锁定目标的特定情况,否则我在这里没有更多可以提到的内容。

似乎你链接到Paul的答案已经失效了。 - kofifus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接