内存屏障生成器

28

阅读Joseph Albahari的线程教程,以下是被提及为内存屏障生成器的内容:

  • C#的lock语句(Monitor.Enter/Monitor.Exit
  • Interlocked类中的所有方法
  • 使用线程池的异步回调 - 这些包括异步委托、APM回调和Task延续
  • 设置和等待信号构造
  • 任何依赖于信号的东西,例如启动或等待任务

此外,Hans Passant和Brian Gideon 添加了以下内容(假设没有一个已适用于前面的类别):

  • 启动或唤醒线程
  • 上下文切换
  • Thread.Sleep()

我在想这个清单是否完整(如果完整可能实际上也不太可能)

编辑 建议添加:

  • 易失性变量(读取意味着获取栅栏,写入意味着释放栅栏)

这将涉及内存模型。在x86/x64上,每个写操作都是一个栅栏。请阅读Albahari文章中关于Itanium的部分。这个列表对实际应用没有太多用处。 - H H
谢谢,我知道那篇文章。实际上,根据它,在.NET 2中所有的写操作都是写入屏障(无论硬件架构如何)。我对其他.NET暗示的内存屏障很感兴趣。 - Ohad Schneider
1
@ohadsc:类似于x86的“所有写入都是写栅栏”的行为 是微软CLR的特性。 ECMA CLI规范没有提供任何此类保证,我不确定其他实现提供了什么强有力的保证;例如,Mono。 - LukeH
@LukeH - 是的,我应该更具体一些。 - Ohad Schneider
3个回答

40

以下是我对这个主题的看法,并尝试在一个答案中提供一个准确完整的列表。如果我发现其他内容,我将不时地编辑我的回答。

普遍认为会导致隐式屏障的机制:

  • 所有的Monitor类方法,包括C#关键字lock
  • 所有Interlocked类方法。
  • 所有Volatile类方法(.NET 4.5+)。
  • 大多数SpinLock方法,包括EnterExit
  • Thread.Join
  • Thread.VolatileReadThread.VolatileWrite
  • Thread.MemoryBarrier
  • volatile关键字。
  • 任何启动线程或导致委托在另一个线程上执行的操作,包括QueueUserWorkItemTask.Factory.StartNewThread.Start、编译器提供的BeginInvoke方法等。
  • 使用信号机制,如ManualResetEventAutoResetEventCountdownEventSemaphoreBarrier等。
  • 使用调度操作,如Control.InvokeDispatcher.InvokeSynchronizationContext.Post等。

被猜测(但不确定)会导致隐式屏障的机制:

  • Thread.Sleep(由我自己提出,并可能由其他人提出,因为展现内存屏障问题的代码可以通过此方法解决)
  • Thread.Yield
  • Thread.SpinWait
  • Lazy<T>取决于指定的LazyThreadSafetyMode

其他值得一提的内容:

  • 由于它们使用lockInterlocked.CompareExchange,因此C#中的事件具有默认的添加和删除处理程序。
  • x86存储具有释放栅栏语义。
  • 尽管ECMA规范没有要求,但微软对CLI的实现在写入时具有释放栅栏语义。
  • MarshalByRefObject似乎会压制子类中的某些优化,这可能导致出现隐式内存屏障。感谢Hans Passant发现并提醒我。1

1这解释了为什么BackgroundWorker在其CancellationPending属性的基础字段上没有volatile也能正确工作。


2
不错!(+1) 是Hans Passant在这里的评论中提到了上下文切换:https://dev59.com/6ljUa4cB1Zd3GeqPVuUL。关于事件处理程序,lock(this)实际上已被Interlocked实现所取代:https://dev59.com/7HA75IYBdhLWcg3wAUB9#3522556 - Ohad Schneider
很高兴听到这个消息,采用这种方法会少很多麻烦 :) - Ohad Schneider
关于Thread.sleep创建屏障,你能提供一些参考资料吗? - thewpfguy
@thewpfguy:抱歉,我不知道。这就是为什么它一直留在我的猜测列表中的原因。虽然我知道它在幕后是如何工作的,但我几乎可以肯定它总是会生成一个屏障。我确定有时候会生成屏障,因为这很容易证明。 - Brian Gideon
@AfterWorkGuinness:不,我没有。但是,如果它不这样做,事情就不会真正起作用,这是有道理的。此外,没有人真正质疑它是否如此,因此除非有相反的证据出现,否则可以安全地假设它是这样的。 - Brian Gideon
显示剩余5条评论

12

我记得 Thread.VolatileRead 和 Thread.VolatileWrite 方法的实现实际上会导致全栅栏,而不是半栅栏。

这非常不幸,因为人们可能无意中依赖于这种行为;他们可能编写了需要全栅栏的程序,认为只需要半栅栏,认为得到的是半栅栏,如果这些方法的实现确实提供了半栅栏,他们将面临令人不快的惊喜。

我建议避免使用这些方法。当然,在涉及低锁定代码时,我会避免一切,因为除了最简单的情况外,我不够聪明正确地编写它。


当然,这只适用于琐碎的情况(例如我链接到的线程中所描述的情况)。我可以向您保证,我也不够聪明 :) - Ohad Schneider
查看 C#4 中 VolatileRead/Write 的 BCL 代码,似乎只设置了半栅栏(即仅在读取前调用 Thread.MemoryBarrier(),并且仅在写入后调用)。当然,我可能误解了您所说的半栏杆和全栏杆之间的区别。 - dlev
2
@dlev:在弱内存模型中,全面的MemoryBarrier比通常读取volatile字段时执行load-with-acquire IL指令具有更强大且更昂贵的效果。 - Eric Lippert
就个人而言,我更喜欢使用那些在访问发生的地方突出显示访问行为的方法,并避免使用volatile(在字段所在的地方突出显示访问行为,可能是代码中的多行)。正如你所说,事实上,目前的情况比看起来更安全(因此,如果有实现变更,可能会突然变得不太安全)。这也更加昂贵(因为虽然我在真实世界的使用中也避免使用低锁定代码,除非我确实有明确的收益,但在进行有趣的实验时,我喜欢优化到愚蠢的程度)。 - Jon Hanna

3

没问题,我会把它加入列表中。 - Ohad Schneider
据我所知,volatile会导致所有读/写操作在读取/写入volatile变量之前被执行。或者我错了吗,@configurator? - Leonard Brünings
从Albahari的教程中:volatile关键字指示编译器在每次读取该字段时生成一个获取屏障,并在每次写入该字段时生成一个释放屏障 - Ohad Schneider
1
我可能错了。让我适当地说明一下我的评论:就我所知,volatile并不会导致内存屏障,但确实可以防止读写重排序;内存屏障在某种意义上比volatile字段的读写更强有力的承诺。 - configurator
@Damokles,@ohadsc:请看Eric在这里对他自己的回答发表的评论 - 意思是:“内存屏障比读取易失性字段具有更强的效果”。 - configurator
显示剩余2条评论

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