人们普遍接受(我相信!)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
,但我很想知道是否 必要 。
答案 0 :(得分:17)
由于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 spec会说明以下内容:
12.6.5锁和线程:
获取锁(
System.Threading.Monitor.Enter
或进入同步方法)将隐式执行易失性读操作,并且释放锁(System.Threading.Monitor.Exit
或保留同步方法)将隐式执行易失性写操作。见§12.6.7。
12.6.7易失性读写:
易失性读取具有“获取语义”,这意味着保证在CIL指令序列中的读取指令之后发生的对存储器的任何引用之前发生读取。易失性写入具有“释放语义”,这意味着写入保证在CIL指令序列中的写入指令之前的任何存储器引用之后发生。
此外,这些博客条目还有一些可能感兴趣的细节:
答案 1 :(得分:4)
除了Michael Burr的答案之外,Wait
不仅释放并重新获得锁定,而且还这样做,以便另一个线程可以取出锁定以检查共享状态并调用{{1 }}。如果第二个线程没有取出锁定,则Pulse
将抛出。如果他们不Pulse
,则第一个帖子的Pulse
将不会返回。因此,任何其他线程对共享状态的访问必须在适当的内存条件下发生。
因此,假设根据本地可检查规则使用Wait
方法,则所有内存访问都发生在锁内,因此只有Monitor
的自动内存屏障支持是相关/必要的
答案 2 :(得分: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
不是......我希望这是值得的(但你可能已经考虑过了)。