【问题标题】:Double lock with volatile or memory barrier具有易失性或内存屏障的双锁
【发布时间】:2018-02-28 17:20:46
【问题描述】:

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

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 来完成什么?

【问题讨论】:

  • 我什至不确定您要问什么。你只是想实现一个单例吗?
  • 一些 Microsoft 员工将永远无法完全从处理安腾处理器中恢复过来。不幸的是,他们用 volatile 来驯服野兽的废话也出现在 C# 规范中。今天一切都无关紧要。使用 Lazy 类。
  • @FrankQ。 “执行当前线程的处理器不能以这样一种方式重新排序指令,即调用 MemoryBarrier 之前的内存访问在调用 MemoryBarrier 之后的内存访问之后执行。”因此,如果没有 MemoryBarrier,安腾处理器将重新排序指令。
  • 链接的 MSDN 文章存在根本缺陷。单独的内存屏障是无用的——你需要另一个屏障来读取。

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


【解决方案1】:

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

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

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

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

C# lock 关键字不处理这个问题吗?

锁在其中一个读取上被删除。

锁阻止其他线程甚至读取任何内容。

锁在其中一个读取上被删除。

Barriers 只会维持顺序,但如果仍然从某个缓存中读取该值会怎样。 volatile 还不需要吗?

从缓存中读取相当于将读取时间向后移动;由易变或显式障碍引起的障碍对如何观察这种向后移动施加了限制。

因为 C# lock 本身就设置了屏障,那里真的需要屏障吗?

锁在其中一个读取上被删除。

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

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

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

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

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

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

【讨论】:

  • “我不喜欢使用线程” - 无价之宝!大多数问题都不是这样解决的。如果您的问题无法在 1 个“线程”上解决,x“线程”不会以任何因素改善它。
  • “不要自己解决这些问题,让专家为你解决这些问题。”——那么,你永远不会成为专家。我更喜欢:给定时间,尝试解决这些问题以了解专家处理的内容;没有时间,至少要说明问题并选择安全第一。
  • @acelent:这种通常明智的方法的问题在于,芯片制造商给了我们一个在多线程程序中没有任何东西可以正常工作的世界。在过去的十年中,我对 C# 的内存模型进行了很多 的思考,并得出结论我对它的理解还不够好,无法编写非平凡的解决方案并确信它们是正确的。我尽量避免跨线程共享内存;如果我不能,那么我完全遵循既定的正确模式,而不是试图推理记忆模型的疯狂世界。
  • 好的,想象一下Mathematics 的某个人像你一样享有盛誉,他说“不要自己解决这些问题;让专家为你解决这些问题。”“不要去那里。我不喜欢 ;” 任何数学科目。 Stack Overflow 绝对是这些主题的网站,不像Software Engineering(或Physics),这个建议可能更贴切。除此之外,我同意你在这个问题上发布和评论的内容。
  • @acelent:如果数学界的某个人说“你的 P=NP '证明' 甚至没有开始解决专家几十年来已知的此类证明中的两个问题;不要去那里,这是浪费你的时间和每个人的时间,你会为你的曲柄证明而烦恼”,那么我会将其视为一项公共服务。我在这里给出可靠、实用的工程建议。 我们作为语言设计师失败了。我们构建的系统过于复杂而无法正确使用,并且未能提倡更简单、更安全的系统。我正在尽我所能减轻这种情况。
【解决方案2】:

正如其他人所提到的,您只需使用 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 在我的回答中的 cmets,了解这些方法之间的主要区别以及可能有助于您在其中一种或另一种之间做出决定的因素。

【讨论】:

  • 根本没有理由使用Lazy,只需直接初始化静态字段,因为它已经安全了。整个解决方案只需要public static Singleton Value {get;} = new Singleton();
  • @Servy:虽然我完全同意你的看法——在 99.999% 的情况下这样做是正确的——但还是有区别的。静态字段初始化器在第一次访问 class 时运行;当第一次访问 property 时运行惰性操作。如果存在这样一种情况:操作非常昂贵并且可能在访问属性之前很久就访问了该类,那么使用惰性初始化器可能是值得的。跨度>
  • @EricLippert 因此,如果单例类的操作涉及访问单例实例,使用Lazy 是一个有用的功能(它不是,真的,因为在这种情况下 true 解决方案是将逻辑移出类,因为它显然与单例是分开的)。在任何地方使用 Lazy,即使您永远不需要它,也不会产生任何效果。
  • 我已经更新了我的答案以反映使用简单的静态初始化程序
猜你喜欢
  • 2019-11-09
  • 1970-01-01
  • 1970-01-01
  • 2017-07-31
  • 2010-12-19
  • 2017-01-17
  • 1970-01-01
  • 2020-02-08
  • 2014-09-11
相关资源
最近更新 更多