1

这是 - 据我所知 - 一个严格的学术/理论问题。

据我了解(Vance Morrison, http: //msdn.microsoft.com/en-us/magazine/cc163715.aspx,“无法创建易失性内存访问......”),volatileC# 中的关键字——除此之外-- 防止通过“代码运动”引入相应字段的任何额外读取。

有一个不使用 C# 的双重检查锁定的变体 ( http://blogs.msdn.com/b/brada/archive/2004/05/12/130935.aspxvolatile ) :

public sealed class Singleton
{
   private static Singleton value;
   private static object syncRoot = new object();

   public static Singleton Value
   {
      get
      {
         if (value == null)
         {
            lock (syncRoot)
            {
               if (value == null)
               {
                  Singleton newVal = new Singleton();
                  Thread.MemoryBarrier(); // ensures that all is ready to publish
                  value = newVal;
               }
            }
         }

         return value;
      }
   }     
}

但是,如果我们只假设 ECMA 内存模型(而不是更强大的 .NET 2.0 内存模型),我想知道这个变体是否会失败?换句话说(通过尝试与 Morrison 的一种构造进行类比,在他的图 8 下),假设代码如下所示:

public sealed class Singleton
{
   private static Singleton value;
   private static object syncRoot = new object();

   public static Singleton Value
   {
      get
      {
         Singleton register = value; // read the field into a register
         if (value == null) // re-read the field (for some unknown reason)
         {
            lock (syncRoot)
            {
               if (value == null)
               {
                  Singleton newVal = new Singleton();
                  Thread.MemoryBarrier();
                  value = register = newVal; // update the register and the field
               }
            }
         }

         return register; // return the reference from the register
      }
   }     
}

在第二个代码示例中,如果线程在将尚未初始化的字段读入寄存器后立即被抢占,并且如果线程在其他线程初始化该字段后的某个时间恢复,则该属性将返回 null。如果有人在代码中以这种方式编写它(使用名为 的局部变量register),我会认为它被破坏了。但是“代码运动”——在理论上,如果不是在实践中——是否可以有效地将第一个样本(在代码中)变成第二个样本(用于执行)?我相信volatile将防止这种情况。而且我不知道为什么任何现实世界的系统都可能在将值复制到寄存器后重新读取该字段,但这不是我要驱动的重点。我想我的问题真的归结为:在 ECMA 内存模型下,上面隐含的转换(将第一个样本转换为第二个样本)是否有任何无效(作为“代码运动”的问题)?

在第二个代码示例中,从单个线程的角度来看,在读取寄存器时,寄存器肯定与字段保持相同的引用。但是我们知道我们可能正在查看一个线程共享的字段,如果代码运动引入对该字段的读取是有效的,那么我们是否会遇到读取该字段可能会看到不同值的可能性从登记册目前持有的那一份?如果没有任何东西(如volatile)表明这将是一个问题,那么实际上会阻止代码运动以这种方式交错读取一个字段和一个(假设的)寄存器(除了没有任何理由这样做) ? 进入lock(及其嵌入MemoryBarrier) 应该(大概)表明一个寄存器——一个真正的寄存器,而不是一个代表寄存器的局部变量——可能是陈旧的,但是如果线程永远不会进入lock(由于前面的重复读取)呢?

显然这很难测试,因为 .NET 内存模型提供了更强的保证。

4

0 回答 0