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 &amp;&amp; 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 是不是确保重新读取字段?的主要内容,如果未能解决你的问题,请参考以下文章

不了解 Monitor.Pulse() 的必要性

c# Monitor.wait() 经典实例

C#简单理解 Monitor.Wait 与 Monitor.Pulse

Monitor.Wait,条件变量

如何通知解锁线程(Monitor.Wait(),PulseAll()模拟)

lock与monitor的区别