【问题标题】:Memory barrier generators内存屏障生成器
【发布时间】:2011-09-28 17:43:53
【问题描述】:

阅读Joseph Albahari's threading tutorial,以下是内存屏障的生成器:

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

此外,Hans Passant 和 Brian Gideon added the following(假设其中没有一个已经属于前面的类别之一):

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

我想知道这个列表是否完整(如果甚至可以实际制作完整的列表)

编辑建议添加:

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

【问题讨论】:

  • 这将是关于Memory Models。在 x86/x64 上,每个 Write 都是一个栅栏。阅读 Albahari 文章中关于安腾的部分。此列表不会有太多实际用途。
  • 谢谢,我知道那篇文章。实际上,根据它,在 .NET 2 中,所有写入都是写入栅栏(无论硬件架构如何)。我对其他 .NET 隐含的内存障碍感兴趣。
  • @ohadsc:类似于 x86 的“所有写入都是写入栅栏”行为是 Microsoft CLR 的一个特性。 ECMA CLI 规范不提供任何此类保证,我不确定其他实现提供了哪些强有力的保证;例如,单声道。
  • @LukeH - 是的,我应该更具体一些

标签: c# memory-barriers


【解决方案1】:

这是我对这个主题的看法,并试图在一个答案中提供一个准完整的列表。如果我遇到任何其他人,我会不时编辑我的答案。

普遍认为会造成隐性障碍的机制:

  • 所有Monitor 类方法,包括C# 关键字lock
  • 所有Interlocked 类方法。
  • 所有 Volatile 类方法 (.NET 4.5+)。
  • 大多数SpinLock 方法包括Enter 和Exit。
  • Thread.Join
  • Thread.VolatileRead 和 Thread.VolatileWrite
  • Thread.MemoryBarrier
  • volatile 关键字。
  • 启动线程或导致委托在另一个线程上执行的任何操作,包括 QueueUserWorkItem、Task.Factory.StartNew、Thread.Start、编译器提供的 BeginInvoke 方法等。
  • 使用ManualResetEvent、AutoResetEvent、CountdownEvent、Semaphore、Barrier等信号机制。
  • 使用诸如Control.Invoke、Dispatcher.Invoke、SynchronizationContext.Post等封送操作。

推测(但不确定)会导致隐性障碍的机制:

  • Thread.Sleep(由我自己和可能其他人提出,因为存在内存屏障问题的代码可以用这种方法修复)
  • Thread.Yield
  • Thread.SpinWait
  • Lazy<T> 取决于指定的 LazyThreadSafetyMode

其他值得注意的提及:

  • 默认添加和删除 C# 中事件的处理程序,因为它们使用 lock 或 Interlocked.CompareExchange。
  • x86 商店有释放栅栏语义
  • 尽管 ECMA 规范没有强制要求,但 Microsoft 的 CLI 实现已在写入时释放栅栏语义。
  • MarshalByRefObject 似乎抑制了子类中的某些优化,这可能使其看起来好像存在隐式内存屏障。感谢 Hans Passant 发现并引起我的注意。1

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

【讨论】:

  • 不错! (+1) Hans Passant 在他的评论中提到了上下文切换:stackoverflow.com/q/6574389/67824。关于事件处理程序,lock(this) 实际上已替换为 Interlocked 实现:stackoverflow.com/questions/3522361/…
  • 我开始考虑内存屏障的方式是,如果 2 个线程可以在相同的确切时间访问某些碎片状态 - a需要内存屏障(最好是锁)。否则,为防止并发而设置的任何机制(例如,仅在线程 A 访问共享状态后才发出信号、等待、启动线程 B)可能会产生所需的内存屏障。你同意这种方法吗?
  • @ohadsc:我没有意识到添加/删除处理程序现在是用Interlocked.CompareExchange 实现的。不错的收获!
  • @ohadsc:是的,我想我基本上同意这种说法。
  • 我很高兴听到这个消息,这种方法的头痛要少得多:)
【解决方案2】:

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

这是非常不幸的,因为人们可能会在不知不觉中依赖这种行为;他们可能编写了一个需要全栅栏的程序,认为他们需要半栅栏,认为他们得到的是半栅栏,如果这些方法的实现确实提供了半栅栏,他们将会大吃一惊。

我会避免使用这些方法。当然,我会避免所有涉及低锁定代码的事情,除了最琐碎的情况外,我不够聪明,无法正确编写代码。

【讨论】:

  • 当然,这是针对琐碎的情况(例如我链接到的线程中描述的情况)。我可以向你保证我也不够聪明:)
  • 查看 VolatileRead/Write (C#4) 的 BCL 代码,看起来只设置了半栅栏(即仅在读取之前调用 Thread.MemoryBarrier() 并且仅写之后。)当然,我可能只是误解了你所说的半栅栏和全栅栏的意思。
  • @dlev:与读取 volatile 字段时通常执行的加载获取 IL 指令相比,完整的 MemoryBarrier 在弱内存模型中具有更强且更昂贵的效果.
  • 就我个人而言,我想使用这些方法(访问行为在访问发生的地方突出显示)并避开volatile(访问行为在哪里突出显示该字段可能是许多行代码)。不过,正如您所说,从目前的情况来看,这比看起来更安全(因此,实施更改可能会突然变得不那么安全)。它也更昂贵(因为虽然我也避免在实际使用中使用低锁定代码,除非我有明确的收获,但我确实喜欢在尝试乐趣时优化到愚蠢的程度)。
【解决方案3】:

volatile 关键字也充当内存屏障。见http://blogs.msdn.com/b/brada/archive/2004/05/12/130935.aspx

【讨论】:

  • 不错,我会把它添加到列表中
  • volatile 不会导致内存屏障。在链接中,内存屏障用于防止重新排序,但这并不意味着如果您阻止重新排序,您就会获得内存屏障!
  • AFAIK volatile 导致在读/写 volatile 变量之前执行所有读/写操作。还是我弄错了@configurator?
  • 来自 Albahari 的教程:volatile 关键字指示编译器在每次读取该字段时生成一个获取栅栏,并在每次写入该字段时生成一个释放栅栏
  • 我可能是错的。让我适当地限定我的评论:据我所知,volatile 不会造成内存障碍,但会阻止读取和写入的重新排序;从某种意义上说,内存屏障比 volatile 字段读取或写入更有保证。
猜你喜欢
  • 2014-02-04
  • 1970-01-01
  • 2018-12-06
  • 1970-01-01
  • 2011-07-05
  • 1970-01-01
  • 2014-05-29
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多