具有易失性或内存屏障的双锁

Double lock with volatile or memory barrier

这是我使用的代码,运行 几个月没有出现任何问题。

public sealed class Singleton 
{
    private static Singleton value;
    private static object syncRoot = new Object();
    public static Singleton Value 
    {
        get 
        {
            if (Singleton.value == null) 
            {
                lock (syncRoot) 
                {
                    if (Singleton.value == null) 
                    {
                        Singleton.value = new Singleton();
                    }
                }
            }
            return Singleton.value;
        }
    }      
}

但是,我遇到了这个 link,它概述了上述问题。

a) 写入 Singleton.value = new Singleton(); 可能会缓存在处理器上,因此其他线程可能最终看不到它。要修复此问题,请使用 volatile 关键字。

Q(1):C# lock 关键字不处理这个问题吗?

b) 在同一篇文章中概述了另一个更好的解决方案,然后是避免 volatile 并在写入 Singleton.value.

之后引入 System.Threading.Thread.MemoryBarrier();

问题:

Q(2) 不太明白写完后需要MemoryBarrier()。什么可能的重新排序可能会导致另一个线程将 Singleton.value 视为 null ? lock 甚至阻止其他线程读取任何内容。

Q(3) 屏障只会维持秩序,但如果仍然从某个缓存中读取值会怎样。 volatile 还需要吗?

Q(4) 因为 C# lock 本身就放置了屏障,所以那里真的需要屏障吗?

最后, 我是否需要使用这两种方法更新我的代码,或者它是否足够好?

编辑 有一个答案建议使用 Lazy 初始化。我知道了。

但是他们试图使用锁不能保证的 volatie 和 memorybarrier 来完成什么?

正如其他人提到的,您可以通过使用 Lazy 为您生成实例来省去很多麻烦:

public sealed class Singleton 
{
    private static Lazy<Singleton> _value = new Lazy<Singleton>(() => new Singleton());

    public static Singleton Value => _value.Value;
}

正如比我聪明得多的人指出的那样,大多数时候这可以通过使用静态初始化程序来进一步简化:

public sealed class Singleton 
{
    public static Singleton Value = new Singleton();
}

请参阅 Eric Lippert 对我的回答的评论,了解这些方法之间的主要区别以及可能有助于您在其中做出决定的因素。

This is the code I have in place, running for couple of months with no issues.

如果有十亿分之一的失败几率,并且代码 运行 每天在一千台机器上运行一千次,那么平均每年都会发生一次无法调试的严重故障三年。

如果它只在特定硬件上失败,而您在 x86 上进行所有测试,那么您将永远不会看到失败。

没有测试低锁代码正确性这样的事情。代码要么被证明是正确的,要么不是。你不能依赖测试。

Doesn't the C# lock keyword take care of this?

在其中一个读取中删除了锁。

The lock prevents other threads from even reading anything.

在其中一个读取中删除了锁。

Barriers will just maintain order but what if the value is still read from some cache instead. Isn't volatile still required ?

从缓存中读取相当于在时间上向后移动读取;由易变或明确的障碍引起的障碍对如何观察这种倒退运动施加了限制。

Is barrier really required there since C# lock itself places it ?

在其中一个读取中删除了锁。

Do I need to update my code with either approach or is it good enough?

我永远不会写这样的代码。如果需要惰性初始化,使用Lazy<T>。如果您需要单例,请使用单例模式的标准实现。不要自己解决这些问题;让专家为您解决这些问题

But what were they trying to accomplish using volatile and memorybarrier that lock doesn't guarantee ?

他们试图正确地消除锁,从而在非竞争路径中节省几纳秒。与难以调试的罕见严重故障的成本相比,这些纳秒对您的用户有多大价值?

任何时候你试图删除一个锁,你都会完全沉浸在低级内存模型的疯狂世界中。您必须假设所有记忆都在不断变化,除非有什么东西使它保持静止;您必须假设内存访问的任何和所有合法重新排序都是可能的,即使是在大多数硬件上不可能的那些。您不知道将来会发明什么奇怪的硬件并用于 运行 您的代码。

不要去那里。我不喜欢使用线程;如果我想并行化某些东西,我的首选是使用虚拟机、容器或进程来解决问题。如果你必须使用线程,尽量不要共享内存。如果您必须共享内存,请使用专家构建的最高级别构造,如 TaskLazy,而不是自己推出内存障碍和互锁操作。