为什么在C#中局部变量不能是volatile的?

18
public void MyTest()
{
  bool eventFinished = false;

  myEventRaiser.OnEvent += delegate { doStuff(); eventFinished = true; };
  myEventRaiser.RaiseEventInSeperateThread()

  while(!eventFinished) Thread.Sleep(1);

  Assert.That(stuff);
}
为什么eventFinished不能是volatile的?这有关紧凑代码中运行时优化的问题。在while循环中,编译器或运行时可能会变得太聪明,知道eventFinished只能为false。特别是当你考虑一个被提升的变量作为类的成员和委托作为同一类的方法时,这种情况就会出现。这将剥夺优化器eventFinished曾经是局部变量的事实。

2
你的变量在这种情况下不是本地变量!相反,它是编译器生成的类中的实例变量。 - Anton Tykhyy
4
这是一个语义上的区别——在代码方面,它是一个被捕获的本地变量… - Marc Gravell
1
我所看到的是一个在不同线程中更新的局部变量。尽管我知道编译器会将我的局部变量转换为实例变量,但预编译器显然没有这样做,或者太“固执”以承认它。 - Patrick Huizinga
1
我不明白这不是C#编译器的错误在哪里?还是说这仅仅是var捕获的一个陷阱?无论如何,这个陷阱与var捕获匿名方法中的几个其他不太显眼的陷阱结合在一起,真的把C#搞得一团糟。它一开始非常棒,但现在变得和C++一样糟糕/更糟糕(我认为它更糟糕,因为这些副作用很奇怪,不直观且无用)。如果MSFT在实现这些编译器特性之前花费更多精力进行适当的设计,那么它完全没有必要如此糟糕。 - Zach Saw
C#已经变得过于复杂,这只是另一个例子。 - Zach Saw
4个回答

12

有一个线程原语ManualResetEvent,可以精确地完成这个任务——你不想使用布尔标志。

像这样的代码应该能够完成任务:

public void MyTest()
{
    var doneEvent = new ManualResetEvent(false);

    myEventRaiser.OnEvent += delegate { doStuff(); doneEvent.Set(); };
    myEventRaiser.RaiseEventInSeparateThread();
    doneEvent.WaitOne();

    Assert.That(stuff);
}

关于本地变量不支持volatile关键字的问题,我认为在理论上没有任何原因导致它不可能在 C# 中被支持。很可能之所以不支持只是因为在 C# 2.0 之前没有使用这样的特性。现在,随着匿名方法和 lambda 函数的出现,这种支持有可能变得更加有用。如果我漏掉了什么,请有经验的人士帮忙澄清。


2
需要Eric的时候他在哪里?;-p - Marc Gravell
18
如果你的线程和锁定是如此复杂、性能敏感和处理器顺序敏感,以至于你需要将一些东西标记为volatile,那么你不应该依赖编译器来为你把局部变量重写成闭包类的字段等各种疯狂操作。你应该明确地编写代码,让它尽可能清晰精准,以便将代码的确切语义传达给需要理解它的维护程序员。语法糖的目的是隐藏机制,而你想要暴露机制! - Eric Lippert
1
如果在 doneEvent.WaitOne(); 之前发生了 doneEvent.Set();,会发生什么? - Zanoni
2
本地变量本身无法被两个不同的线程访问,正如您所指出的那样,但是闭包创建的字段可以。将本地变量标记为volatile可能表明由闭包生成的字段也将被标记为相同的volatile。 - Noldorin
7
从这个角度来看,语言不保证提升的局部变量被实现为字段。它们可以被实现为数组的元素、属性或任何其他形式,只要语言能够在运行时生成维护闭包和变量语义的代码即可。我们是否想向语言添加一个功能,限制编译器编写者生成代码的方式?尤其是因为这个功能本身就是个坏主意。提升的局部变量已经是一个漏洞百出的抽象;我们不想让它更糟。 - Eric Lippert
显示剩余7条评论

10
在大多数情况下,局部变量是特定于线程的,所以与volatile相关的问题是完全没有必要的。
但像您的示例一样,当它是一个“captured”变量时——当它被静默实现为编译器生成的类上的字段时,情况就会改变。因此,在理论上它可能是volatile的,但在大多数情况下,这不值得增加额外的复杂性。
特别是像Monitor(又名lock)和Pulse之类的东西可以很好地处理它,任何其他线程构造也都可以。
线程处理比较棘手,主动循环很少是管理它的最佳方法...
关于编辑... secondThread.Join() 是显而易见的选择,但如果你真的想使用单独的令牌,请参见以下内容。与诸如ManualResetEvent之类的东西相比,这种方式的优点是它不需要从操作系统中请求任何内容 - 它纯粹在CLI内部处理。
using System;
using System.Threading;
static class Program {
    static void WriteLine(string message) {
        Console.WriteLine(Thread.CurrentThread.Name + ": " + message);
    }
    static void Main() {
        Thread.CurrentThread.Name = "Main";
        object syncLock = new object();
        Thread thread = new Thread(DoStuff);
        thread.Name = "DoStuff";
        lock (syncLock) {
            WriteLine("starting second thread");
            thread.Start(syncLock);
            Monitor.Wait(syncLock);
        }
        WriteLine("exiting");
    }
    static void DoStuff(object lockHandle) {
        WriteLine("entered");

        for (int i = 0; i < 10; i++) {
            Thread.Sleep(500);
            WriteLine("working...");
        }
        lock (lockHandle) {
            Monitor.Pulse(lockHandle);
        }
        WriteLine("exiting");
    }
}

如果RaiseEventInSeperateThread()被有效地实现为:new Thread(() => { Thread.sleep(100); OnEvent(); };,你会如何使用Monitor或lock让MyTest()等待事件委托执行完毕? - Patrick Huizinga
我是这样的意思:new Thread(() => { Thread.sleep(100); OnEvent(); }).Start(); - Patrick Huizinga
你会有一个线程 WaitOne 和另一个线程 Pulse。稍后我会尝试添加一个例子... - Marc Gravell

9

如果你想让本地变量表现得像Volatile,你也可以使用Volatile.Write。例如:

public void MyTest()
{
  bool eventFinished = false;

  myEventRaiser.OnEvent += delegate { doStuff(); Volatile.Write(ref eventFinished, true); };
  myEventRaiser.RaiseEventInSeperateThread()

  while(!Volatile.Read(eventFinished)) Thread.Sleep(1);

  Assert.That(stuff);
}

1
回答不错,但你的 while 循环应该使用 Volatile.Read - CoderBrien

2
如果事件触发时,直到进程退出该本地变量的作用域后才完成,会发生什么?该变量将被释放,您的线程将失败。
明智的做法是附加一个委托函数,指示父线程子线程已完成。

代码没问题;那是一个“捕获”的变量,并且被实现为编译器生成类的字段。线程不会失败。 - Marc Gravell
但是类级别的变量也可能出现同样的情况,如果实例在其他线程中的事件完成之前被垃圾回收,那么就会出现相同的问题。 - Noldorin
只要它仍然在任一线程的可见范围内,就无法进行垃圾回收。 - Marc Gravell
1
@Marc:是的,我在评论后才考虑到这一点。尽管如此,在C# 2.0中,易变的局部变量似乎对局部变量很有用。 - Noldorin
我不明白垃圾回收器怎么可能在这里“抢风头”。MyTest()方法正在执行一个半- while(true){}循环,同时等待结果。唯一可能发生的坏事是如果事件委托忘记将eventFinished设置为true,从而导致MyTest()挂起。幸运的是,我足够聪明,不会忘记。;-) - Patrick Huizinga

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接