内存屏障生成器

Posted

技术标签:

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

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

C# 的lock 语句(Monitor.Enter/Monitor.ExitInterlocked 类的所有方法 使用线程池的异步回调——包括异步委托、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 - 是的,我应该更具体一些 【参考方案1】:

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

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

所有Monitor 类方法,包括C# 关键字lock 所有Interlocked 类方法。 所有 Volatile 类方法 (.NET 4.5+)。 大多数SpinLock 方法包括EnterExitThread.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

其他值得注意的提及:

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

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

【讨论】:

不错! (+1) Hans Passant 在他的评论中提到了上下文切换:***.com/q/6574389/67824。关于事件处理程序,lock(this) 实际上已替换为 Interlocked 实现:***.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 字段读取或写入更有保证。

以上是关于内存屏障生成器的主要内容,如果未能解决你的问题,请参考以下文章

内存屏障是如何工作的?

Linux 内核 内存管理优化内存屏障 ④ ( 处理器内存屏障 | 八种处理器内存屏障 | 通用内存屏障 | 写内存屏障 | 读内存屏障 | 数据依赖屏障 | 强制性内存屏障 |SMP内存屏障 )

Linux 内核 内存管理优化内存屏障 ④ ( 处理器内存屏障 | 八种处理器内存屏障 | 通用内存屏障 | 写内存屏障 | 读内存屏障 | 数据依赖屏障 | 强制性内存屏障 |SMP内存屏障 )

Linux 内核 内存管理优化内存屏障 ② ( 内存屏障 | 编译器屏障 | 处理器内存屏障 | 内存映射 I/O 写屏障 )

内存屏障后和互锁操作后内存缓存一致性的时序

解密内存屏障