【问题标题】:Do I need this field to be volatile?我需要这个字段是易变的吗?
【发布时间】:2012-04-25 00:20:20
【问题描述】:

我有一个线程旋转,直到另一个线程更改的 int 是某个值。

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

是否需要将 this.m_cur 声明为 volatile 才能使其工作?由于编译器优化,这可能会永远旋转吗?

【问题讨论】:

  • 将 int 设为属性并在 setter 方法中向线程发出信号(可能是 autoResetEvent)。绕过了问题,减少了 CPU 使用率,消除了不稳定的疑虑。
  • 除了极少数情况外,这通常是个坏主意;你碰巧知道你希望旋转最多多少微秒吗?
  • CPU-looping while read 'cur' that iswritten by another thread will fail to detect cur out-of-limit if the polling thread is not running when the setter thread write an out-of-limit价值。如果它在过载的盒子上被抢占,它必须等待平均半个量子才能检测到超出范围的电流。如果 cur 在轮询器未运行时再次回到范围内,则根本不会检测到超出范围的情况。
  • @EricLippert 它只会在另一个线程的这两行代码之间旋转,所以它应该非常快。旋转的轮询线程也是一个低于正常的优先级。 if(Interlocked.Add(ref this.m_cur, x) > this.Max) this.m_cur = this.Max;

标签: c# .net multithreading volatile


【解决方案1】:

是的,这是一个硬性要求。允许即时编译器将 m_cur 的值存储在处理器寄存器中,而无需从内存中刷新它。 x86 抖动实际上有,x64 抖动没有(至少在我最后一次查看时)。

需要 volatile 关键字来抑制这种优化。

易失性在 Itanium 内核上意味着完全不同的东西,这是一种内存模型较弱的处理器。不幸的是,这就是它成为 MSDN 库和 C# 语言规范的原因。这对 ARM 内核意味着什么还有待观察。

【讨论】:

  • 我喜欢你回答他问题的方式,而不是像许多人喜欢的那样只是说“用另一种方式做”。根据他的需要,一个信号可能更有效,但他的问题不是“......或者什么更好?”另外,我相信很多人在这里学到了一些新东西。谢谢。
  • +1:顺便说一下,我们确定属性会阻止 JIT 编译器进行提升优化吗?该属性将很简单,因此它可能会首先内联。我想可以进行两阶段优化:inline-lift。我在规格中没有看到任何可以排除这种情况的东西,但也许我看起来并不那么努力。
  • @Brian - 我对在需要 volatile 但没有任何同步的情况下使用属性感到困惑时发现了这一点。 BackgroundWorker.CancellationPending 是一个很好的例子,一个布尔值。这在我所知道的任何地方都没有描述,volatile 的语义记录非常差。在 .NET 之前,它们一直都是。
  • @payo - 这就是为什么我没有提供答案,只是在评论中提出建议。将限制检查移至信号线程意味着在满足条件之前根本不需要线程间通信。
  • @BrianGideon - 已确认。我找到了 BackgroundWorker 不会出错的原因,它继承了 MarshalByRefObject,因此 CancellationPending 属性不会被内联。感谢您坚持您的观点:)
【解决方案2】:

下面的博客有一些关于 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 读取 非易失性字段可能会观察到过时的值:编译器优化 和处理器优化。

【讨论】:

  • 完全不使用轮询循环似乎更安全。
  • 在大多数情况下是正确的。虽然我想也有理由旋转锁,例如避免产生线程的上下文切换。
  • 避免上下文切换的麻烦在于它并不总是那么容易。此外,OP 代码不是 clssic 布尔标志自旋锁。如果盒子因为就绪线程多于核心而过载,则轮询线程可能会被抢占并且暂时不运行。在此期间,它无法检测到“cur”超出限制。如果 cur 在轮询器运行之前回到限制范围内,则完全错过了超出限制条件。
【解决方案3】:

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

一般来说,使用 Interlocked 来修改跨线程共享的原子值是更可取的选择。它不仅为您解决了内存屏障问题,而且还往往比其他同步选项快一点。

也就是说,就像其他人所说的那样,轮询循环非常浪费。最好暂停需要等待的线程,让修改 m_cur 的人在时机成熟时负责唤醒它。 Monitor.Wait() and Monitor.Pulse()AutoResetEvent 都可能非常适合这项任务,具体取决于您的具体需求。

【讨论】:

  • 轮询循环解决方案无论如何都不能可靠地工作,至少在过载的盒子上不能。 setter 线程可以在轮询器未运行时写入超出范围的“cur”。如果 'cur' 在轮询器运行之前再次回到范围内,则根本不会检测到超出范围的情况。
猜你喜欢
  • 1970-01-01
  • 2015-06-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-06-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多