具有易失性或记忆障碍的双重锁定

时间:2017-09-19 19:44:47

标签: c# .net multithreading volatile memory-barriers

这是我已有的代码,运行几个月没有任何问题。

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关键字。

问(1):C#lock关键字没有解决这个问题吗?

b)在同一篇文章中概述的另一个更好的解决方案是避免volatile并在写入System.Threading.Thread.MemoryBarrier();后引入Singleton.value

问题

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

Q(3)障碍只会维持秩序,但如果仍然从某些缓存中读取该值,该怎么办呢?仍然不需要挥发性吗?

Q(4)因为C#lock本身会放置屏障,所以确实需要屏障吗?

最后, 我是否需要使用任何一种方法更新我的代码,还是足够好?

编辑 有人建议使用Lazy初始化。我知道了。

但是他们试图使用lockie和memorybarrier来实现锁定并不能保证什么?

2 个答案:

答案 0 :(得分:9)

  

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

如果有一个十亿分之一的失败机会,并且代码每天在一千台机器上运行一千次,那么平均每三年就有一次不可能调试的关键故障。

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

没有测试低锁代码的正确性。代码可以证明是正确的,或者不是。你不能依赖测试。

  

C#lock关键字不能解决这个问题吗?

在其中一个读数上省略了锁定。

  

锁可以防止其他线程读取任何内容。

在其中一个读数上省略了锁定。

  

障碍只会维持秩序,但如果仍然从某些缓存中读取值,该怎么办呢?是不是仍然需要挥发性?

从缓存中读取相当于及时向后移动读取;由挥发性或显性障碍引起的障碍限制了如何观察这种向后运动。

  

因为C#lock本身就存在障碍,所以需要屏障吗?

在其中一个读数上省略了锁定。

  

我是否需要使用这两种方法更新我的代码,还是足够好?

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

  

但是他们试图使用volatile和memorybarrier实现锁定并不能保证什么?

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

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

不要去那里。我不喜欢使用线程;如果我想并行化某些东西,我的偏好是在问题上抛出虚拟机,容器或进程。如果必须使用线程,请尝试不共享内存。如果必须共享内存,请使用由专家构建的最高级别构造,例如TaskLazy,而不是滚动自己的内存障碍和互锁操作。

答案 1 :(得分:3)

正如其他人所说,只需使用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对我的答案的评论,以了解这些方法之间的关键区别,以及可能有助于您在其中一种方法之间作出决定的内容。