Monitor.Wait是否确保重新读取字段?

22
通常认为(我相信!)lock会强制重新加载字段中的任何值(本质上充当内存屏障或栅栏-在这个领域中我的术语有点宽松,恐怕),因此仅在lock内部访问的字段本身不需要是volatile。(如果我已经错了,请说!)
提出了一个很好的评论在这里提出,质疑如果代码执行Wait()是否同样适用-即一旦它被Pulse()d,它会从内存中重新加载字段,还是可能在寄存器(等等)中。
更简单地说:在Wait()之后恢复时,字段是否需要是volatile以确保获取当前值?
查看反射器,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 Saffron
1
@Sam - 不,我可以给你展示一个volatile在x86上很重要的例子:https://dev59.com/lHRB5IYBdhLWcg3w9Lvn#458193 - Marc Gravell
2
顺便说一句,可能与此无关,但如果其他线程不断向队列中添加项目,使其计数永远不会降为零,那么TryDequeue将无法通知调用者有关关闭的信息(例如,工作线程将继续工作)。所以也许应该是while (!closing && queue.Count == 0) Monitor.Wait(queue),然后在循环外重新检查closing - Daniel Earwicker
@Earwicker - 目的是使用Close()来停止排空,因此这是可以预料的;如果队列关闭,Enqueue可以轻松修改以抛出异常。 - Marc Gravell
3个回答

18

由于Wait()方法释放并重新获取Monitor锁,如果lock执行内存屏障语义,则Monitor.Wait()也会执行。

为了解决您的问题:

Monitor.Wait()的锁定行为在文档中有说明(http://msdn.microsoft.com/en-us/library/aa332339.aspx),重点如下:

当线程调用Wait时,它会释放对象上的锁并进入对象的等待队列。对象就绪队列中的下一个线程(如果有)将获取锁并独占该对象。所有调用Wait的线程都保留在等待队列中,直到它们收到来自锁的所有者发送的Pulse或PulseAll信号。如果发送了Pulse,则只影响等待队列头部的线程。如果发送了PulseAll,则影响等待该对象的所有线程。当接收到信号时,一个或多个线程离开等待队列并进入就绪队列。就绪队列中的线程被允许重新获取锁。

此方法在调用线程重新获取对象上的锁时返回

如果您询问是否有关于lock /已获取的Monitor是否意味着内存屏障的参考资料,ECMA CLI规范如下所述:
12.6.5 锁和线程:
获得锁(System.Threading.Monitor.Enter或进入同步方法)应隐式执行易失性读取操作,并释放锁(System.Threading.Monitor.Exit或离开同步方法)应隐式执行易失性写入操作。请参见§12.6.7。
12.6.7 易失性读取和写入:
易失性读取具有“获取语义”,这意味着读取保证发生在CIL指令序列中读取指令之后发生的任何对内存的引用之前。易失性写入具有“发布语义”,这意味着写入保证发生在CIL指令序列中写入指令之前的任何内存引用之后。
此外,这些博客条目提供了一些可能感兴趣的详细信息:

那是我的隐含假设,但我希望能有某种引用/参考资料...? - Marc Gravell
+1 因为这基本上就是我要说的(尽管我添加了一些额外的推理)。 - Daniel Earwicker
这并没有解决问题。问题是JIT代码生成,而不是缓存/内存行为。方法调用如何防止JITter生成将变量存储在寄存器中的代码? - Hans Passant
@nobugz - 一个方法调用可以做到这一点。JIT 可以轻松识别并将 Monitor.Lock 函数视为特殊指标。在 C++ 中也是如此:您可以编写类似于对 MemoryBarrier 的函数调用的内容,实际上只是内联了一些汇编的宏:xchg ...,而编译器在看到它时知道要小心处理。 - Daniel Earwicker
@Earwicker:也许可以。它能吗?并且它是否适用于每种架构?您是否在任何地方看到过这方面的文档,以便我们可以信赖它?或者缺乏这样的文档需要使用volatile吗? - Hans Passant
显示剩余7条评论

4
除了Michael Burr的回答之外,Wait不仅会释放并重新获取锁,而且还会这样做,以便另一个线程可以取出锁来检查共享状态并调用Pulse。如果第二个线程没有取出锁,则Pulse会抛出异常。如果他们不Pulse第一个线程的Wait就不会返回。因此,任何其他线程对共享状态的访问都必须在适当的内存屏障场景中发生。
因此,假设Monitor方法按照可本地检查的规则使用,则所有内存访问都发生在锁内部,因此只有lock的自动内存屏障支持是相关/必要的。

1

这次也许我可以帮你一下……不用使用volatile,你可以用一个整数和Interlocked.Exchange来替代。

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更直接) - Marc Gravell
使用Monitor Wait/Pulse模式是更简单的方法。等待循环是等待共享可变状态的更改。因此,任何影响该等待结果的事情都必须在锁定内进行修改,并且必须调用“Pulse”。对于对“closing”的修改也是如此。 - Daniel Earwicker
现在是凌晨3点,我在想:http://meta.stackexchange.com/questions/11652/how-addicted-to-stack-overflow-are-you - Kiril
只为你而存在。世界是一个广阔的地方。 - Marc Gravell
是格林威治标准时间上午9点,这是最重要的时区。 :p - Daniel Earwicker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接