C#中的Volatile和Thread.MemoryBarrier

12
为了实现一个多线程应用的无锁代码,我使用了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 循环依赖于其条件中的该值;那么在 while 循环之前放置 Thread.MemoryBarrier() 是否是修复此问题的正确方法?例如:

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 Said
@Jalal,你是想使用volatile/memorybarrier这种方法来解决问题,还是对任何能解决问题的方法都可以接受? - Aaron McIver
@Aron;我正在尝试使用volatile/memorybarrier方法来解决这个问题。 - Jalal Said
2
不要将volatile与无锁代码混淆。在任何情况下,volatile都不能使您的代码线程安全。 - Darin Dimitrov
你正在使用哪个版本的 .NET? - Conrad Frix
显示剩余3条评论
5个回答

11

"C# In a Nutshell"的说法是正确的,但它的陈述是无意义的。为什么呢?

  • 在单个线程内,如果“写入”对逻辑产生影响,那么后跟“读取”的操作将保证按程序顺序执行,即使没有使用“volatile”关键字。
  • 在多线程程序中,“写入”之前的“读取”在您的示例中完全不需要担心。

让我们澄清一下。接下来是您原始代码:

private void A() 
{ 
    //… 
    if (_foo) 
    { 
        //do something 
    } 
}
如果线程调度器已经检查了变量_foo,但在//do something注释之前挂起该线程会发生什么?那么此时,您的另一个线程可以更改_foo的值,这意味着您所有的volatile和Thread.MemoryBarriers都无效了!如果非常重要,如果_foo的值为false,则必须避免执行do_something,则除了使用锁之外别无选择。
但是,如果当_foo突然变为false时,可以执行do something,那么这意味着volatile关键字已经足够满足您的需求。
明确一点:所有告诉您使用内存屏障的回答者都是不正确的或过于保守。

1
感谢您的回复; 在我的情况下,如果 _foo 为真,我希望尽可能避免执行 do_something,但这并不是非常关键的;但如果我可以使用 MemoryBarrier 让它更准确,那么为什么不与 volatile 一起使用呢?如果线程调度在 _foo 检查后暂停线程“这是可能的”,那么 do_somthing 将被执行,但至少我们减少了 _foo 在那个时候为假的机会,对吧? - Jalal Said
2
@Jalal:volatile声明本身已经保证变量不会被存储在CPU寄存器中。它保证直接内存读写(这使得多处理器机器上其他处理器的缓存无效)。因此,显式的内存屏障是不必要的;volatile已经实现了你需要的功能。 - Brent Arias
@BrentArias MemoryBarrier是必要的,因为处理器可能会在一个处理器上更新值而不在另一个处理器上更新,因此一个线程读取更新后的值,而另一个线程读取过时的值。 - Anthony

5

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

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

仅使用volatile关键字不会创建临界区,并且它不会使线程自动与彼此同步。

在编写无锁代码时,您应该非常小心。这并不简单,即使专家也很难做到正确无误。
无论您试图解决的原始问题是什么,都很可能存在更合理的解决方法。


@Liran:我知道仅使用volatile不会创建临界区,也不会使线程相互同步。 我只是要求在无锁代码中的任何时间点,当任何线程请求_foo变量时,它必须更新到最新值。 - Jalal Said
@Jalal,将变量声明为volatile将确保每个加载操作都会读取变量的最新值(例如从主内存而不是寄存器)。你不应该在代码中到处散布内存屏障...这样做可能会对性能产生重大负面影响。再次强调,如果你不只是想玩一下并尝试锁定自由代码,而且实际上有真正的生产代码...我鼓励你找到另一个解决方案。 - Liran
@Liran:将变量声明为volatile将确保每个加载操作都读取变量的最新值。正如我从http://www.albahari.com/threading/part4.aspx了解到的那样,写入后跟随的读取可能无法获取最新值,我正在尝试使用无锁代码进行实验。问题是:在易失性变量的任何get操作之前执行`Thread.MemoryBarrier`,对应用程序的性能是否有很大负面影响?如果没有,我可以在变量的任何get之前将其放在属性中。请查看问题更新说明。 - Jalal Said
3
如果两个操作(例如对X加载和对Y存储)之间没有数据依赖关系,那么一个存储操作紧接着一个读取操作可能会被重新排序。请确保您理解CLR的内存模型。Joe Duffy在此总结了规则:http://www.bluebytesoftware.com/blog/2007/11/10/CLR20MemoryModel.aspx 此外,使用属性与代码的线程安全性无关。 - Liran
@Liran:谢谢你提供的链接,我一定会阅读它。这里的属性不是为了线程安全,而是为了在请求时保证值的新鲜度。如果使用volatile保证值始终新鲜无论任何事情是很好的,但如果不需要并且Thread.MemoryBarrier()没有问题,那为什么不在属性中使用它呢? - Jalal Said
@Jalal,使用属性不会以任何方式影响数据的“新鲜度”。属性只是作为一个getter/setter方法。如果您不需要完整的栅栏(Thread.MemoryBarrier),请不要使用它。否则,您将遭受不必要的性能损失。在继续之前,请确保您了解什么是内存模型、获取/释放语义、栅栏等。 - Liran

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

如果这解决了问题,那么将Thread.MemoryBarrier()放在while循环的闭括号前的最后一行并在while循环的调用之前。 - Jalal Said
1
@Jalal:我相当确定在这些示例中不需要显式的MemoryBarrier调用,只要将_foo标记为volatile即可。(虽然我不是专家;我可能会使用lock。) - LukeH
3
在这种情况下,使用 Thread.MemoryBarrier,无论是在之前、之后还是两种情况都使用,都没有实现比 volatile 更高的效果。 - Brent Arias

1
这里提取...
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();
    }
}

1
@ Aaron,谢谢。我之前查看过那个链接,它在同一本书“C# 4.0 in a Nutshell”中。但是,如果这解决了问题,那么将Thread.MemoryBarrier()放在while循环的闭括号之前的最后一行应该可以。 - Jalal Said
1
@Aaron:如果_foo被标记为volatile,那么我相信在这些特定的示例中,显式的MemoryBarrier调用是不必要的。(我相信但不是100%确定,如果有疑问,我可能会使用lock。) - LukeH
1
是的。我表述不正确,条件在循环期间会发生变化。因此,您希望障碍尽可能晚地出现。 - H H
1
“不想‘锁定’那么长时间”。memoryBarrier调用是短暂的,没有锁定或其他状态涉及。它更像是清空管道。 - H H
2
@Aaron:你提供的链接涉及重新排序问题。发帖者的代码示例没有展示任何重新排序问题。发帖者所需的只是将_foo的值直接读写到内存中,而不是寄存器。volatile关键字本身就足够实现这一点。 - Brent Arias
显示剩余6条评论

0

关于内存屏障的 Microsoft 官方说明:

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

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


请参考http://www.albahari.com/threading/part4.aspx,以查看微软自己的说法是不正确的!! - Jalal Said
1
海报特别要求无锁实现。 - Brent Arias

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