线程安全Singleton:为什么内存模型不能保证其他线程会看到新实例?

Gil*_*lad 7 c# multithreading

我在Jon的Skeet在线页面上读到了如何在C#中创建一个线程安全的Singleton

http://csharpindepth.com/Articles/General/Singleton.aspx

// Bad code! Do not use!
public sealed class Singleton
{
    private static Singleton instance=null;

    private Singleton()
    {
    }

    public static Singleton Instance
    {
        get
        {
            if (instance==null)
            {
                instance = new Singleton();
            }
            return instance;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

在此代码下面的段落中,它说:

如前所述,上述内容不是线程安全的.两个不同的线程都可以评估测试if(instance == null)并发现它是真的,然后两个创建实例,这违反了单例模式.请注意,实际上可能已经在计算表达式之前创建了实例,但是内存模型不保证其他线程可以看到实例的新值,除非已经传递了合适的内存屏障.

你能否解释为什么内存模型不能保证其他线程看不到实例的新值?

静态变量位于堆上,但为什么不立即与其他线程共享?我们是否需要等待上下文切换,以便其他线程知道实例不再为空?

Jon*_*eet 14

你能否解释为什么内存模型不能保证其他线程看不到实例的新值?

内存模型很复杂,目前还没有非常明确的文档记录,但从根本上来说,很少有情况可以安全地依赖于一个线程在另一个线程上"看到"而没有某些锁定或其他线程间的线程所写的值沟通继续.

例如,考虑一下:

// Bad code, do not use
public class BigLoop
{
    private static bool keepRunning = true;

    public void TightLoop()
    {
        while (keepRunning)
        {
        }
    }

    public void Stop()
    {
        keepRunning = false;
    }
}
Run Code Online (Sandbox Code Playgroud)

如果你创建了两个线程,它调用一个TightLoop而另一个来电Stop,也不能保证该循环方法将永远终止.

现代CPU中有许多级别的缓存,并且要求每次读取都返回到主内存将消除大量优化.所以我们有内存模型,可以保证哪些变化在什么情况下肯定是可见的.除了这些保证之外,允许JIT编译器假设实际上只有一个线程 - 因此它可以将字段的值缓存在寄存器中,并且永远不会再次访问主存储器,例如.

当前记录的内存模型严重不足,并且表明一些明显奇怪的优化应该是有效的.我不会去远了这条路线,但它是值得一读的乔·达菲的博客文章CLR 2.0内存模型.(这比记录的ECMA内存模型更强大,但是博客文章并不是这类关键文档的理想位置,我认为仍然需要更清晰.)

静态变量位于堆上,但为什么它不与其他线程共享?

与其他线程共享-但价值并不一定会立即可见.

  • @Gilad:不要考虑处理器和内存总线以及上下文切换等等.这些是实施细节可能会有变化.根本问题在于,C#做出了一些关于如何根据线程启动和锁定等特殊事件来命令读取和写入的承诺.这些承诺不包括承诺两个线程将观察到读取和写入*的一致排序,除非通过内存模型的保证强制*. (8认同)
  • @Gilad:因为*很多*潜在的原因.也许JIT编译器先前插入了一个读取并将值缓存在寄存器或堆栈中.也许这是由于CPU.也许你实际上是在运行时执行,其中所有内容都由多台联网的计算机组成,并且它们只在必要时才传输数据.我担心内存模型是一个非常复杂的领域. (3认同)
  • @Gilad不,它不会.我们总是立即进行交易,以提高编译器,JITer和处理器级别的代码性能.但他们放置了像Memory Barriers和Lock这样的工具来禁用这些性能改进,以解决Jon在他的回答中提到的潜在错误. (2认同)