内存屏障生成器
Posted
技术标签:
【中文标题】内存屏障生成器【英文标题】: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 - 是的,我应该更具体一些 【参考方案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 在他的评论中提到了上下文切换:***.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 写屏障 )