两个线程之间的共享变量与共享属性的行为不同

dmg*_*dmg 6 .net c# concurrency volatile thread-safety

在他关于C#中线程的优秀论文中,Joseph Albahari提出了以下简单程序来演示为什么我们需要对多个线程读取和写入的数据使用某种形式的内存屏障.如果您在发布模式下编译它并在没有调试器的情况下自由运行它,程序永远不会结束:

  static void Main()
  {
     bool complete = false;
     var t = new Thread(() =>
     {
        bool toggle = false;
        while (!complete) toggle = !toggle;
     });
     t.Start();
     Thread.Sleep(1000);
     complete = true;                  
     t.Join(); // Blocks indefinitely
  }
Run Code Online (Sandbox Code Playgroud)

我的问题是,为什么以上稍微修改过的上述程序版本不再无限期地阻塞?

class Foo
{
  public bool Complete { get; set; }
}

class Program
{
  static void Main()
  {
     var foo = new Foo();
     var t = new Thread(() =>
     {
        bool toggle = false;
        while (!foo.Complete) toggle = !toggle;
     });
     t.Start();
     Thread.Sleep(1000);
     foo.Complete = true;                  
     t.Join(); // No longer blocks indefinitely!!!
  }
}
Run Code Online (Sandbox Code Playgroud)

以下仍然无限期地阻止:

class Foo
{
  public bool Complete;// { get; set; }
}

class Program
{
  static void Main()
  {
     var foo = new Foo();
     var t = new Thread(() =>
     {
        bool toggle = false;
        while (!foo.Complete) toggle = !toggle;
     });
     t.Start();
     Thread.Sleep(1000);
     foo.Complete = true;                  
     t.Join(); // Still blocks indefinitely!!!
  }
}
Run Code Online (Sandbox Code Playgroud)

如下所示:

class Program
{
  static bool Complete { get; set; }

  static void Main()
  {
     var t = new Thread(() =>
     {
        bool toggle = false;
        while (!Complete) toggle = !toggle;
     });
     t.Start();
     Thread.Sleep(1000);
     Complete = true;                  
     t.Join(); // Still blocks indefinitely!!!
  }
}
Run Code Online (Sandbox Code Playgroud)

Eri*_*lje 7

在第一个示例中Complete是一个成员变量,可以在每个线程的寄存器中进行缓存.由于您没有使用锁定,因此可能无法将对该变量的更新刷新到主存储器,而另一个线程将看到该变量的过时值.

在第二个示例中,where Complete属性是,您实际上是在Foo对象上调用一个函数来返回一个值.我的猜测是,虽然简单变量可以缓存在寄存器中,但编译器可能并不总是以这种方式优化实际属性.

编辑:

关于自动属性的优化 - 我不认为规范在这方面有任何保证.您实质上是在考虑编译器/运行时是否能够优化getter/setter.

如果它在同一个对象上,它似乎就是这样.在另一种情况下,它似乎没有.无论哪种方式,我都不会赌它.解决这个问题的最简单方法是使用一个简单的成员变量,标记是volotile为了确保它始终与主内存同步.


Tej*_*ejs 5

这是因为在你提供的第一个片段中,你创建了一个关闭布尔值的lambda表达式complete- 因此,当编译器重写它时,它会捕获值的副本,而不是引用.同样,在第二个中,它由于关闭Foo对象而捕获引用而不是副本,因此当您更改基础值时,由于引用而注意到更改.