这个问题源于没有volatile的编译器(理论上可以通过各种方式优化任何变量,包括将其存储在CPU寄存器中。)而文档表示,在使用同步(如锁定变量)时不需要这样做。但在某些情况下,编译器/jit似乎不可能知道您是否会在代码路径中使用它们。因此,人们怀疑这里确实发生了其他事情,使记忆模型"起作用"。
在这个例子中,是什么阻止编译器/jit将_count优化到寄存器中,从而在寄存器上完成增量,而不是直接写入内存(稍后在退出调用后写入内存)?如果_count是volatile,那么看起来一切都应该很好,但很多代码都是在没有volatile的情况下编写的。如果编译器在方法中看到锁或同步对象,那么它可以知道不优化寄存器中的_count。。但在这种情况下,锁调用在另一个函数中。
大多数文档都说,如果使用锁之类的同步调用,就不需要使用volatile。
那么,是什么阻止编译器将_count优化到寄存器中,并可能只更新锁中的寄存器呢?我有一种感觉,由于这个确切的原因,大多数成员变量都不会被优化到寄存器中,因为每个成员变量都需要是可变的,除非编译器可以告诉它不应该优化(否则我怀疑大量的代码会失败)。几年前,当我研究C++时,我看到了类似的东西,局部函数变量被存储在寄存器中,类成员变量没有。
所以主要的问题是,编译器/jit不会把类成员变量放在寄存器中,因此volatile就没有必要了,这真的是在没有volatile的情况下唯一可行的方法吗?
(请忽略呼叫中缺乏异常处理和安全性,但您了解要点。)
public class MyClass
{
object _o=new object();
int _count=0;
public void Increment()
{
Enter();
// ... many usages of count here...
count++;
Exit();
}
//lets pretend these functions are too big to inline and even call other methods
// that actually make the monitor call (for example a base class that implemented these)
private void Enter() { Monitor.Enter(_o); }
private void Exit() { Monitor.Exit(_o); } //lets pretend this function is too big to inline
// ...
// ...
}
输入和离开Monitor
会导致内存不足。因此,CLR确保Monitor.Enter
/Monitor.Exit
之前的所有写入操作对所有其他线程都可见,并且方法调用之后的所有读取操作都"发生"在它之后。这也意味着调用之前的语句不能在调用之后移动,反之亦然。
请参阅http://www.albahari.com/threading/part4.aspx.
这个问题的最佳答案似乎是,在调用任何函数之前,存储在CPU寄存器中的任何变量都会保存到内存中。这是有道理的,因为从单个线程的编译器设计角度来看需要这样做,否则,如果其他函数/方法/对象使用该对象,则该对象可能看起来不一致。因此,可能并不像一些人/文章所说的那样,同步对象/类是由编译器检测到的,非易失性变量通过它们的调用是安全的。(可能是在使用锁或同一方法中的其他同步对象时,但一旦在另一个方法中调用了调用这些同步对象的调用,则可能不会),相反,仅调用另一种方法就可能足以将存储在CPU寄存器中的值保存到内存中。因此,不需要所有变量都是可变的。
此外,我怀疑和其他人也怀疑,由于一些线程问题,类的字段没有得到优化。
一些注意事项(我的理解):Thread.MemoryBarrier()主要是一条CPU指令,以确保从CPU的角度来看,写入/读取不会绕过障碍。(这与存储在寄存器中的值没有直接关系)因此,这可能不是直接导致将变量从寄存器保存到内存的原因(除了根据我们在这里的讨论,它是一个方法调用,这可能会导致这种情况发生-它可能真的是任何方法调用,尽管可能会影响从寄存器保存的所有类字段)
从理论上讲,JIT/编译器也可以在同一方法中考虑该方法,以确保变量存储在CPU寄存器中。但是,只要遵循我们提出的对另一个方法或类的任何调用的简单规则,就会将存储在寄存器中的变量保存到内存中。此外,如果有人将该调用封装在另一个方法中(可能有许多方法是深度的),编译器就不太可能分析那么深来推测执行情况。JIT可以做一些事情,但它可能不会深入分析,而且这两种情况都需要确保锁/同步在任何情况下都能工作,因此最简单的优化是可能的答案。
除非我们有编写编译器的人能够证实这一点,否则这一切都是猜测,但这可能是我们对为什么不需要volatile的最佳猜测。
如果遵循该规则,同步对象只需要在进入和离开时使用自己对MemoryBarrier的调用,以确保CPU从其写缓存中具有最新的值,从而刷新它们,从而可以读取正确的值。在这个网站上,你会看到这就是所谓的内隐记忆障碍:http://www.albahari.com/threading/part4.aspx
那么,是什么阻止编译器将_count优化到寄存器中呢并且可能只更新锁中的寄存器?
据我所知,文件中没有任何内容可以阻止这种情况的发生。关键是对Monitor.Exit
的调用将有效地保证_count
的最终值将在完成时提交给内存。
编译器可能知道不将_count优化为如果在方法中看到锁定或同步对象,请注册。。但在这种情况下,锁调用在另一个函数中。
从您的角度来看,通过其他方法获取和释放锁这一事实是无关紧要的。模型内存定义了一组非常严格的规则,这些规则必须遵守内存屏障生成器。将这些Monitor
调用放在另一个方法中的唯一结果是JIT编译器将更难遵守这些规则。但是,JIT编译器必须遵守;时期如果方法调用变得复杂或嵌套太深,那么我怀疑JIT编译器在这方面可能会采用任何启发式方法,并说:"算了,我不会优化任何东西!"
所以主要的问题是,这真的是唯一可行的方法吗如果没有volatile,编译器/jit就不会放入类成员那么寄存器中的变量和易失性是不必要的吗?
它之所以有效,是因为协议也是在读取_count
之前获取锁。如果读者不这样做,那么所有的赌注都会被取消。