Monitor.Wait 是不是确保重新读取字段?
Posted
技术标签:
【中文标题】Monitor.Wait 是不是确保重新读取字段?【英文标题】:Does Monitor.Wait ensure that fields are re-read?Monitor.Wait 是否确保重新读取字段? 【发布时间】:2011-01-26 17:42:02 【问题描述】:普遍接受(我相信!)lock
将强制重新加载字段中的任何值(基本上充当内存屏障或围栏 - 我在这方面的术语有点松散,我害怕),结果是只有在lock
中访问的字段曾经 本身不需要是volatile
。
(如果我已经错了,请直说!)
一个很好的评论是raised here,质疑如果代码执行Wait()
是否也是如此 - 即一旦它已经是Pulse()
d,它是否会从内存中重新加载字段,或者它们是否可以在寄存器中(等)。
或者更简单点:字段是否需要volatile
才能保证在Wait()
之后恢复时获取到当前值?
查看反射器,Wait
向下调用到ObjWait
,即managed internalcall
(与Enter
相同)。
有问题的场景是:
bool closing;
public bool TryDequeue(out T value)
lock (queue) // arbitrary lock-object (a private readonly ref-type)
while (queue.Count == 0)
if (closing) // <==== (2) access field here
value = default(T);
return false;
Monitor.Wait(queue); // <==== (1) waits here
...blah do something with the head of the queue
显然,我可以直接将其设为volatile
,或者我可以将其移出,以便在每次出现脉冲时退出并重新输入Monitor
,但我很想知道是否有任何一个是 需要。
【问题讨论】:
Intel x86 和 x64 具有 CPU 缓存一致性,易失性仅在 itanium 上很重要,因此测试这一点在后面会很痛苦。 @Sam - 不,我可以向您展示一个 x86 示例,其中volatile
很重要:***.com/questions/458173/…
BTW 可能无关紧要,但如果其他线程继续将项目放入队列,因此它的计数永远不会降为零,那么TryDequeue
将无法让其调用者知道关闭(例如,工作人员将保持在工作)。所以也许应该是while (!closing && queue.Count == 0) Monitor.Wait(queue)
,然后在循环外重新检查closing
。
@Earwicker - 意图是Close()
用于停止排水,所以这是意料之中的; Enqueue
可以被简单地修改为在队列关闭时抛出异常。
【参考方案1】:
由于Wait()
方法正在释放和重新获取Monitor
锁,如果lock
执行内存栅栏语义,那么Monitor.Wait()
也会执行。
希望能解决您的评论:
Monitor.Wait()
的锁定行为在文档 (http://msdn.microsoft.com/en-us/library/aa332339.aspx) 中,强调:
当线程调用Wait时,它会释放对象上的锁并进入对象的等待队列。对象就绪队列中的下一个线程(如果有的话)获取锁并独占使用该对象。所有调用
Wait
的线程都保留在等待队列中,直到它们收到来自锁的所有者发送的来自Pulse 或PulseAll
的信号。如果发送Pulse
,则只有等待队列头部的线程受到影响。如果发送PulseAll
,所有等待对象的线程都会受到影响。当收到信号时,一个或多个线程离开等待队列,进入就绪队列。允许就绪队列中的线程重新获取锁。当调用线程重新获得对象上的锁时,该方法返回。
如果您要询问lock
/acquired Monitor
是否暗示内存屏障的参考,ECMA CLI spec 会说明以下内容:
12.6.5 锁和线程:
获取锁(
System.Threading.Monitor.Enter
或进入同步方法)应隐式执行易失性读取操作,释放锁(System.Threading.Monitor.Exit
或离开同步方法)应隐式执行易失性写入操作。请参阅第 12.6.7 节。
12.6.7 易失性读写:
易失性读取具有“获取语义”,这意味着读取保证发生在 CIL 指令序列中读取指令之后发生的任何对内存的引用之前。易失性写入具有“释放语义”,这意味着写入保证发生在 CIL 指令序列中写入指令之前的任何内存引用之后。
此外,这些博客条目还包含一些可能感兴趣的细节:
http://blogs.msdn.com/jaredpar/archive/2008/01/17/clr-memory-model.aspx http://msdn.microsoft.com/msdnmag/issues/05/10/MemoryModels/ http://www.bluebytesoftware.com/blog/2007/11/10/CLR20MemoryModel.aspx【讨论】:
这是我的隐含假设,但我希望得到某种引用/参考...? +1 因为这基本上就是我要说的(尽管我添加了一些额外的推理)。 这并不能解决问题。问题是 JIT 代码生成,而不是缓存/内存行为。方法调用如何防止 JITter 生成将变量存储在寄存器中的代码? @nobugz - 方法调用可以做到这一点。 JIT 可以轻松识别Monitor.Lock
函数并将其视为特殊指标。在 C++ 中也是如此:您可以编写一些看起来像对 MemoryBarrier
的函数调用的东西,它实际上只是一个内联一些程序集的宏:xchg ...
,编译器知道在看到它时会小心处理。跨度>
@Earwicker:也许可以。可以?每个架构都这样做吗?你有没有在任何地方看到过这个文档,所以我们可以依靠它?或者缺少此类文档是否需要使用 volatile?【参考方案2】:
对于 Michael Burr 的回答,Wait
不仅释放并重新获取锁,而且这样做是为了让另一个线程可以取出锁以检查共享状态并调用 Pulse
。如果第二个线程没有取出锁,那么Pulse
将抛出。如果他们不Pulse
第一个线程的Wait
将不会返回。因此,任何其他线程对共享状态的访问必须发生在适当的内存限制场景中。
因此假设Monitor
方法是根据本地可检查规则使用的,那么所有内存访问都发生在锁内,因此只有lock
的自动内存屏障支持是相关/必要的。
【讨论】:
【参考方案3】:这次也许我可以帮助您...您可以使用带有整数的Interlocked.Exchange
,而不是使用volatile
。
if (closing==1) // <==== (2) access field here
value = default(T);
return false;
// somewhere else in your code:
Interlocked.Exchange(ref closing, 1);
Interlocked.Exchange
是一种同步机制,volatile
不是...我希望这是值得的(但您可能已经考虑过这一点)。
【讨论】:
确实,但Monitor
也是一种同步机制;-p(另外:我希望volatile
在这种情况下更直接)
始终使用 Monitor Wait/Pulse 模式更简单。等待循环是等待共享可变状态的更改。所以任何影响等待结果的东西都必须在锁内进行修改,并且必须调用Pulse
。对closing
的修改也是如此。
现在是凌晨 3:00,我想知道:meta.stackexchange.com/questions/11652/…
只为你。世界很大。
格林威治标准时间上午 9 点 - 这是最重要的时区。 :p以上是关于Monitor.Wait 是不是确保重新读取字段?的主要内容,如果未能解决你的问题,请参考以下文章
C#简单理解 Monitor.Wait 与 Monitor.Pulse