【问题标题】:Volatile and Thread.MemoryBarrier in C#C# 中的易失性和 Thread.MemoryBarrier
【发布时间】:2010-11-24 17:09:07
【问题描述】:

要为多线程应用程序实现无锁代码,我使用了volatile 变量, 理论上volatile 关键字只是用来确保所有线程都能看到 volatile 变量的最新值;因此,如果线程A 更新变量值并且线程B 在更新发生后读取该变量,它将看到最近从线程 A 写入的最新值。 正如我在 C# 4.0 in a Nutshell 一书中读到的那样 这是不正确,因为

应用 volatile 不会阻止交换写入后读取。

是否可以通过在每次获取 volatile 变量之前添加 Thread.MemoryBarrier() 来解决此问题,例如:

private volatile bool _foo = false;

private void A()
{
    //…
    Thread.MemoryBarrier();
    if (_foo)
    {
        //do somthing
    }
}

private void B()
{
    //…
    _foo = true;
    //…
}

如果这样可以解决问题;考虑我们有一个 while 循环,该循环在其条件之一处依赖于该值;将Thread.MemoryBarrier() 放在while 循环之前是解决问题的正确方法吗?示例:

private void A()
{
    Thread.MemoryBarrier();
    while (_someOtherConditions && _foo)
    {
        // do somthing.
    }
}

更准确地说,我希望_foo 变量在任何线程随时要求它时提供其最新值;所以如果在调用变量之前插入Thread.MemoryBarrier() 可以解决问题,那么我可以使用Foo 属性而不是_foo 并在该属性的get 中执行Thread.MemoryBarrier() 就像:

Foo
{
    get 
    {
        Thread.MemoryBarrier();
        return _foo;
    }
    set
    {
        _foo = value;
    }
}

【问题讨论】:

  • @Aron;不,它不是重复的;这是另一个问题。
  • @Jalal 您是否尝试使用这种方法 volatile/memorybarrier 来解决问题,或者您是否可以使用任何解决问题的方法?
  • @Aron;我正在尝试使用 volatile/memorybarrier 方法解决这个问题。
  • 不要将 volatile 与无锁代码混淆。在任何情况下,Volatile 都不会使您的代码线程安全。

标签: c# memory-management nonblocking volatile memory-barriers


【解决方案1】:

“C# In a Nutshell”是正确的,但它的说法没有实际意义。为什么?

  • “写”后跟“读”,没有“易失性”,保证无论如何都会按程序顺序发生如果它影响单个线程中的逻辑
  • 在您的示例中,多线程程序中的“读取”之前的“写入”完全没有意义

让我们澄清一下。获取您的原始代码:

private void A() 
{ 
    //… 
    if (_foo) 
    { 
        //do something 
    } 
}

如果线程调度程序已经检查了_foo 变量,但它在//do something 注释之前被挂起,会发生什么?好吧,那时你的其他线程可能会改变_foo 的值,这意味着你所有的 volatiles 和 Thread.MemoryBarriers 都算不了什么!!!如果在_foo 的值为false 时避免do_something 是绝对必要的,那么您别无选择,只能使用锁。

但是,如果 do something 在突然 _foo 变为 false 时可以执行,则意味着 volatile 关键字足以满足您的需求。

需要明确:所有告诉您使用内存屏障的响应者都是不正确的或提供了过度杀伤力。

【讨论】:

  • 感谢您的回复;在我的情况下,如果 _foo 尽可能为真,我想避免执行 do_something,但它不是致命的关键;但是如果我可以使用 MemoryBarrier 使其更准确,那么 为什么不 将它与 volatile 一起使用。如果线程调度在 _foo 检查“女巫是可能的”之后暂停线程,那么 do_somthing 将执行但在至少我们减少了 _foo 在那个时候是假的,对吧?
  • @Jalal:volatile 声明本身已经保证该变量不会存储在 CPU 寄存器中。它保证直接内存读取和写入(这会使多处理器机器上其他处理器的缓存无效)。因此显式内存屏障是不必要的; volatile 已经完成了您需要的工作。
  • @BrentArias MemoryBarrier 是必要的,因为处理器可以在一个处理器上更新值而不是在另一个处理器上,因此一个线程读取更新后的值,另一个线程读取一个过时的值。
【解决方案2】:

这本书正确
CLR 的内存模型表明加载和存储操作可以重新排序。这适用于易失性非易失性变量。

将变量声明为volatile仅意味着加载操作将具有acquire语义,而存储操作将具有release语义。此外,编译器将避免执行某些优化,这些优化依赖于以序列化、单线程方式访问变量这一事实(例如,将加载/存储提升到循环之外)。

单独使用volatile 关键字不会创建临界区,也不会导致线程神奇地相互同步。

在编写无锁代码时应该非常小心。没有什么简单的事情,即使是专家也很难做到。
无论您要解决的最初问题是什么,都可能有更合理的方法来解决。

【讨论】:

  • @Liran:我知道单独使用 volatile 不会创建关键部分,也不会导致线程彼此同步。我只是要求 _foo 变量在任何时候都必须更新值线程在无锁代码中随时要求它。
  • @Jalal,将变量声明为 volatile 将保证每次加载操作都会读取变量的最新值(例如,从主存储器而不是寄存器)。您不应该在整个代码中散布内存屏障……这样做可能会对性能产生重大负面影响。同样,如果您不只是尝试使用无锁代码,并且图片中有真实的生产代码......我鼓励您找到另一个解决方案。
  • @Liran; “将变量声明为 volatile 将保证每次加载操作都会读取变量的最新值”;正如我从书albahari.com/threading/part4.aspx 中了解到的那样,读取后的wirte 可能无法获得最新的值,是的,我正在尝试使用无锁代码;问题是:在对 volatile 变量进行任何获取操作之前执行Thread.MemoryBarrier 是否会对应用程序的性能产生很大的负面影响,如果不是,我可以在获取任何变量之前将其放入属性中。请查看问题更新
  • @Jalal,如果操作之间没有数据依赖关系(例如加载到 X,然后存储到 Y),则可以重新排序存储操作,然后是读取操作。确保您了解 CLR 的内存模型。 Joe Duffy 很好地总结了规则:bluebytesoftware.com/blog/2007/11/10/CLR20MemoryModel.aspx ...此外,您使用属性这一事实与您的代码的线程安全无关。
  • @Liran:感谢您提供的链接,我肯定会阅读它。这里的属性不是为了线程安全,而是为了在请求时提供值的新鲜度,如果使用@的值987654326@ 被保证永远是新鲜的,无论如何,那很好,但如果不是这样,Thread.MemoryBarrier() 也没有问题,那么就不要在财产中使用它吗?
【解决方案3】:

在您的第二个示例中,您还需要将 Thread.MemoryBarrier(); 放入循环中,以确保每次检查循环条件时都获得最新值。

【讨论】:

  • 如果这解决了问题,那么将 Thread.MemoryBarrier() 放置在 while 循环右括号之前和 while 循环调用之前的最后一行。
  • @Jalal:我很确定在这些示例中不需要显式调用MemoryBarrier,只要将_foo 标记为volatile。 (不过我不是专家;我可能只会使用lock。)
  • 在这种情况下:使用 Thread.MemoryBarrier,无论是之前、之后还是两者,都没有实现 volatile 尚未实现的任何东西。
【解决方案4】:

来自here...

class Foo
{
  int _answer;
  bool _complete;

  void A()
  {
    _answer = 123;
    Thread.MemoryBarrier();    // Barrier 1
    _complete = true;
    Thread.MemoryBarrier();    // Barrier 2
  }

  void B()
  {
    Thread.MemoryBarrier();    // Barrier 3
    if (_complete)
    {
      Thread.MemoryBarrier();       // Barrier 4
      Console.WriteLine (_answer);
    }
  }
}

障碍 1 和 4 阻止了此示例 从写“0”。障碍 2 和 3 提供新鲜度保证:他们 确保如果 B 在 A 之后运行,则读取 _complete 将评估为 true。

所以如果我们回到你的循环示例......这就是它应该看起来的样子......

private void A()
{
    Thread.MemoryBarrier();
    while (_someOtherConditions && _foo)
    {
        //do somthing
        Thread.MemoryBarrier();
    }
}

【讨论】:

  • @ Aaron 谢谢我在同一本书“C# 4.0 in a Nutshell”之前检查了该链接。但是如果这解决了问题,那么将 Thread.MemoryBarrier() 放在最后一行在 while 循环的右括号之前。
  • @Aaron:如果_foo 被标记为volatile,那么我很确定在这些特定示例中明确的MemoryBarrier 调用是不必要的。 (非常肯定,但不是 100% 肯定,如果有疑问,我可能会使用 lock。)
  • @Aaron:因为循环必须改变条件,所以障碍应该是循环中的最后一件事。
  • 是的。而我的表述是错误的,循环期间条件会发生变化。因此,您希望尽可能晚地设置障碍。
  • @Aaron:您提供的链接涉及重新订购问题。发帖人的代码示例没有出现任何重新排序问题。海报所需要的只是将 _foo 的值直接读/写到内存而不是寄存器。 volatile 关键字本身就足够了。
【解决方案5】:

微软自己关于内存屏障的话:

仅在内存排序较弱的多处理器系统(例如,采用多个 Intel Itanium 处理器的系统)上才需要MemoryBarrier。

在大多数情况下,C# lock 语句、Visual Basic SyncLock 语句或 Monitor 类提供了更简单的数据同步方式。

【讨论】:

猜你喜欢
  • 2017-08-02
  • 2012-02-22
  • 2012-10-04
  • 1970-01-01
  • 1970-01-01
  • 2023-03-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多