c# - 双重双锁检查的可能不正确的实现

标签 c# concurrency locking

我在我们的项目代码中发现了以下双重锁定检查实现:

  public class SomeComponent
  {
    private readonly object mutex = new object();

    public SomeComponent()
    {
    }

    public bool IsInitialized { get; private set; }

    public void Initialize()
    {
        this.InitializeIfRequired();
    }

    protected virtual void InitializeIfRequired()
    {
        if (!this.OnRequiresInitialization())
        {
            return;
        }

        lock (this.mutex)
        {
            if (!this.OnRequiresInitialization())
            {
                return;
            }

            try
            {
                this.OnInitialize();
            }
            catch (Exception)
            {
                throw;
            }

            this.IsInitialized = true;
        }
    }

    protected virtual void OnInitialize()
    {
        //some code here
    }

    protected virtual bool OnRequiresInitialization()
    {
        return !this.IsInitialized;
    }
}


在我看来,这是错误的实现,因为没有保证不同的线程将看到IsInitialized属性的最新值。

问题是“我对吗?”。

更新:
我担心会发生以下情况:

步骤1.在Processor1上执行Thread1,并将true写入锁部分中的IsInitialized。这次IsInitialized的旧值(错误)在Processor1的高速缓存中。众所周知,处理器具有存储缓冲区,因此Processor1可以将新值(true)放入其存储缓冲区,而不是其高速缓存中。

步骤2. Thread2位于InitializeIfRequired内部,在Processor2上执行并读取IsInitialized。在Processor2的缓存中没有IsInitialized的值,因此Processor2从其他处理器的缓存或内存中询问IsInitialized的值。 Processor1在其缓存中具有IsInitialized的值(但请记住,它是旧值,更新后的值仍在Processor1的存储缓冲区中),因此它将旧值发送到Processor2。结果,Thread2可以读取false而不是true。

更新2:
如果锁(this.mutex)刷新了处理器的存储缓冲区,则一切正常,但是可以保证吗?

最佳答案

由于没有保证不同线程将看到IsInitialized属性的最新值,因此这是错误的实现。问题是“我对吗?”。


您是正确的,这是双重检查锁定的无效实现。您会以多种微妙的方式弄错了为什么错了。

首先,让我们避免您的错误。

在多线程程序中,任何变量的值都“最新颖”的信念是一种错误的信念,这有两个原因。第一个原因是,C#保证了有关如何重新排列读写顺序的某些约束。但是,这些保证不包括任何保证存在全局一致顺序的承诺,并且可以由所有线程推论得出。在C#内存模型中,对变量进行读写操作以及对这些读写操作进行排序约束是合法的。但是,在那些约束不足以完全执行一个读写顺序的情况下,允许所有线程都没有观察到“规范”顺序。允许两个线程同意所有约束均得到满足,但仍然不同意选择什么顺序。从逻辑上讲,每个变量只有一个规范的“最新鲜”值的说法是完全错误的。不同的线程可能会不同意哪个线程比其他线程更“新鲜”。

第二个原因是,即使没有这个怪异的属性,该模型也不允许两个线程在读写顺序上存在分歧,但是在任何低锁程序中都可以读取“最新鲜”的方法仍然是错误的。值。您所拥有的所有原始操作都保证,某些写入和读取不会在时间上向前或向后移动超过代码中的某些点。那里什么也没有说“最新鲜”,无论什么意思。您能说的最好的是,某些读取会读取一个新值。内存模型未定义“最新鲜”的概念。

错误的另一种方法确实非常微妙。您正在根据处理器刷新缓存来推断可能发生的事情,因此做得很好。但是在C#文档中,没有任何地方提到处理器刷新缓存的一句话!这是芯片实现的详细信息,只要您的C#程序在不同的体系结构上运行,该细节就会更改。除非您知道程序将完全在一种体系结构上运行并且您完全了解该体系结构,否则请不要理会处理器刷新缓存。更确切地说,是关于内存模型施加的约束的原因。我知道模型的文档非常缺乏,但这是您应该考虑的事情,因为这实际上是您可以依靠的。

您错的另一种方式是,虽然是,但实现已损坏,但由于您没有读取初始化标志的最新值,因此实现没有中断。问题在于,由标志控制的初始化状态不受时间移动的限制!

让我们让您的示例更加具体:

private C c = null;
protected virtual void OnInitialize()
{
     c = new C();
}


和一个使用站点:

this.InitializeIfRequired();
this.c.Frob();


现在我们来解决真正的问题。没有什么可以阻止IsInitializedc的读取及时移动。

假设线程Alpha和Bravo都在运行此代码。 Thread Bravo赢得比赛,它所做的第一件事是将c读取为null。请记住,允许这样做是因为对读写没有排序限制,因为Bravo永远不会进入锁。

实际上,这将如何发生?允许C#编译器或抖动提前移动读取指令,但不允许这样做。简要地回到缓存体系结构的真实世界,由于c已经在缓存中,因此c的读取可能会在逻辑上移至标志读取的前面。也许它与最近读取的另一个变量接近。或者,分支预测可能正在预测该标志将导致您跳过该锁,而处理器会预取该值。但同样,现实情况是什么也没关系;这就是所有芯片实现的细节。 C#规范允许此读取尽早完成,因此假设在某个时候它将尽早完成!

回到我们的场景。我们立即切换到线程Alpha。

线程Alpha可以按预期运行。可以看到标志表明需要初始化,然后进行锁定,初始化c,设置标志并离开。

现在,线程Bravo再次运行,该标志现在指示不需要初始化,因此我们使用我们先前阅读的c版本,并取消引用null

只要您严格遵循确切的双重检查锁定模式,双重检查锁定在C#中是正确的。一旦您偏离了它,您就会陷入像我刚才描述的那种可怕,不可复制的种族条件错误的杂草中。只是不要去那里:


不要跨线程共享内存。从我刚刚告诉您的所有信息中得出的结论是,我不够聪明,无法编写共享内存并按设计工作的多线程代码。我只够聪明地编写偶然运行的多线程代码,这对我来说是不可接受的。
如果必须跨线程共享内存,请锁定所有访问,无一例外。没那么贵!您知道更昂贵的吗?处理一系列无法​​复制的致命崩溃,这些崩溃都会丢失用户数据。
如果必须在线程之间共享内存,并且必须具有低锁定延迟初始化,那么不要自己编写。使用Lazy<T>;它包含低锁定延迟初始化的正确实现,您可以依靠它在所有处理器体系结构上都是正确的。




后续问题:


  如果锁(this.mutex)刷新了处理器的存储缓冲区,则一切正常,但是可以保证吗?


为了澄清,这个问题是关于在双重检查锁定方案中是否正确读取了初始化标志。让我们在这里再次解决您的误解。

由于已将初始化标志写入锁中,因此可以确保在锁中正确读取了初始化标志。

但是,正如我之前提到的,考虑此问题的正确方法是不对刷新缓存提出任何理由。对此的正确解释是C#规范对如何相对于锁及时移动读写进行了限制。

特别是,锁内部的读操作可能不会移到锁之前,锁内部的写操作可能不会移到锁之后。这些事实与锁提供互斥的事实相结合,足以得出结论:在锁内部读取初始化标志是正确的。

再说一次,如果您不习惯进行此类推论-我可不是! -然后不要编写低锁代码。

关于c# - 双重双锁检查的可能不正确的实现,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53006154/

相关文章:

c# - 如何解释这两个简单循环的性能差异?

c# - 如何在 WPF 中自动检测 Flowdirection RightToLeft 或 LeftToRight

PostgreSQL 和 SELECT FOR UPDATE LIMIT

C# 多线程代码问题损坏数据

c# - Newtonsoft Json 将字典反序列化为 DataContractJsonSerializer 中的键/值列表

c# - 转换和解析 c# 之间的区别

testing - golang中如何测试并发和锁?

concurrency - V-lang : How to send +2500 HTTP requests per second?

php - 限制应用程序中每个组的用户数

java - Java内存模型如何确保所有线程看到的变量值一致?