我需要这个字段变为易失性吗?

9

我有一个线程,它会一直运行,直到另一个线程更改的整数达到特定值。

int cur = this.m_cur;
while (cur > this.Max)
{
    // spin until cur is <= max
    cur = this.m_cur; 
}

这个程序需要将 this.m_cur 声明为易变量才能正常工作吗?是否可能由于编译器优化而导致程序一直处于循环中?


9
将int类型变量改为属性,并在setter方法中信号线程,可以使用AutoResetEvent。这样就能解决问题、降低CPU使用率,同时消除volatile关键字的疑虑。 - Martin James
这通常是一个不好的想法,除了一些罕见的情况;你知道你期望旋转多少微秒吗? - Eric Lippert
当 CPU 循环读取由另一个线程写入的“cur”时,如果轮询线程在 setter 线程写入越限值时没有运行,将无法检测到超出限制的 cur。如果在负载过重的盒子上被抢占,它将需要等待平均半个量子才能检测到超出范围的 cur。如果 cur 在轮询器未运行时再次回到范围内,则根本不会检测到超出范围的情况。 - Martin James
@EricLippert 它将只会在另一个线程的这两行代码之间旋转,因此应该非常快。旋转的轮询线程也是BelowNormal优先级。 if(Interlocked.Add(ref this.m_cur,x)> this.Max) this.m_cur = this.Max; - noctonura
3个回答

12

是的,这是一个严格要求。即时编译器允许将m_cur的值存储在处理器寄存器中,而不刷新它从内存中的值。实际上,x86的jit编译器会这样做,而x64的jit则不会(至少我上次查看的时候是这样)。

volatile关键字是必需的以抑制此优化。

在拥有弱内存模型的Itanium核心上,“volatile”意味着完全不同的事情。不幸的是,这就是进入MSDN库和C#语言规范的内容。在ARM核心上,它将意味着什么还有待观察。


4
我喜欢你回答他的问题的方式,而不是像许多人那样只是说“用另一种方式”。根据他的需求,信号可能更有效,但他的问题并不是“……或者哪个更好?”此外,我相信我们中的许多人在这里学到了新东西。谢谢。 - payo
1
@Brian - 我发现在需要使用volatile但没有任何同步的情况下,属性被用于某些情况下感到困惑。BackgroundWorker.CancellationPending是一个很好的例子,它是一个布尔值。我不知道有没有地方描述这一点,volatile的语义非常缺乏文档支持。在.NET之前,它们一直如此。 - Hans Passant
@BrianGideon - 我也可以让它出错。C# .NET 4 在 i7,64位上。 - Martin James
1
@BrianGideon - 已确认。我找到了这不会出错的原因,因为BackgroundWorker继承自MarshalByRefObject,所以CancellationPending属性不会被内联。感谢您坚持您的观点 :) - Hans Passant
@HansPassant:伙计...还是有些地方不对劲。从我复制的BackgroundWorker代码中删除Component肯定会破坏它。但是,我仍然可以通过对更简单的类进行子类化来演示问题。JIT编译器必须有一些相当复杂的规则来执行这些优化。这也让我想到,在不同的框架版本中,BackgroundWorker实际上可能存在一个非常微妙的错误,正在等待显现。微软真的应该在那个底层字段中添加volatile - Brian Gideon
显示剩余7条评论

4
下面的博客详细介绍了C#中的内存模型。简而言之,使用volatile关键字似乎更安全。 http://igoro.com/archive/volatile-keyword-in-c-memory-model-explained/
来自下面的博客
class Test
{
    private bool _loop = true;

    public static void Main()
    {
        Test test1 = new Test();

        // Set _loop to false on another thread
        new Thread(() => { test1._loop = false;}).Start();

        // Poll the _loop field until it is set to false
        while (test1._loop == true) ;

        // The loop above will never terminate!
    }
}

有两种可能的方法来终止 while 循环:使用锁来保护对 _loop 字段的所有访问(包括读取和写入);将 _loop 字段标记为 volatile。有两个原因导致非 volatile 字段的读取可能会观察到过时的值:编译器优化和处理器优化。

1
似乎完全不使用轮询循环更安全。 - Martin James
大多数情况下是正确的。虽然我想也有使用自旋锁的原因,例如避免让线程放弃上下文切换。 - David Z.
避免上下文切换的问题是,它并不总是容易实现的。此外,OP 代码并不是经典的布尔标志自旋锁。如果因为就绪线程数大于核心数而使箱子过载,轮询线程可能会被抢占并且有一段时间不会运行。在此期间,它无法检测到 'cur' 超出限制。如果在轮询器运行之前 cur 返回了范围内,那么超出限制的条件将被完全忽略。 - Martin James

0

这取决于如何修改m_cur。如果使用普通的赋值语句,例如m_cur--;,那么它确实需要是易失性的。但是,如果使用其中一个Interlocked操作进行修改,则不需要易失性,因为Interlocked的方法会自动插入内存屏障以确保所有线程都能接收到消息。

一般来说,使用Interlocked修改在线程之间共享的原子值是更可取的选择。它不仅为您处理了内存屏障,而且还比其他同步选项更快。

话虽如此,像其他人所说的轮询循环非常浪费资源。最好暂停需要等待的线程,并让修改m_cur的人在时机成熟时负责唤醒它。根据您的具体需求,Monitor.Wait()和Monitor.Pulse()以及AutoResetEvent可能非常适合这个任务。


轮询循环解决方案无论如何都不可靠,至少在负载过重的情况下是这样。当轮询器未运行时,设置线程可能会写入超出范围的“cur”。如果在轮询器运行之前“cur”再次回到范围内,则根本不会检测到超出范围的条件。 - Martin James

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