(我知道他们不会,但我正在寻找实际工作的根本原因而不使用volatile,因为没有什么可以防止编译器将变量存储在没有volatile的寄存器中......或者在那里......)
这个问题源于思想的不和谐,即没有易失性的编译器(可以在理论上以各种方式优化任何变量,包括将其存储在CPU寄存器中。)虽然文档说在使用同步时不需要像锁定变量一样。但实际上在某些情况下,编译器/ jit似乎无法知道您是否会在代码路径中使用它们。因此怀疑是在这里真正发生的其他事情,以使记忆模型"工作"。
在这个例子中是什么阻止编译器/ jit优化_count到寄存器中,从而在寄存器上完成增量而不是直接到存储器(稍后在退出调用后写入内存)?如果_count是易变的,那么看起来一切都应该没问题,但很多代码都是在没有volatile的情况下编写的。如果在方法中看到锁定或同步对象,编译器可能知道不会将_count优化到寄存器中,但在这种情况下,锁定调用是在另一个函数中。
大多数文档都说如果您使用锁等同步调用,则不需要使用volatile。
那么是什么阻止了编译器优化_count到寄存器并可能只更新锁中的寄存器?我有一种感觉,大多数成员变量都不会因为这个原因被优化到寄存器中,因为每个成员变量都需要是易变的,除非编译器告诉它不应该优化(否则我怀疑吨代码会失败)。我在看C ++的时候看到了类似的东西,多年前本地函数变量存储在寄存器中,类成员变量没有。
所以主要的问题是,它是否真的是没有volatile的唯一方法,编译器/ jit不会将类成员变量放入寄存器中,因此不需要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
// ...
// ...
}
答案 0 :(得分:5)
输入和离开Monitor
会导致完整的记忆围栏。因此,CLR确保Monitor.Enter
/ Monitor.Exit
之前的所有写入操作都对所有其他线程可见,并且方法调用之后的所有读取操作都“发生”在它之后。这也意味着在调用之后调用之前的语句不能移动,反之亦然。
答案 1 :(得分:0)
这个问题的最佳猜测答案似乎是,在调用任何函数之前,存储在CPU寄存器中的所有变量都会保存到内存中。这是有道理的,因为来自单个线程的编译器设计观点将需要,否则如果对象被其他函数/方法/对象使用则该对象可能看起来不一致。 因此,有些人/文章声称编译器可以检测到同步对象/类,并且通过调用使非易失性变量变得安全,因此可能没那么多。 (也许它们是在使用锁或同一方法中的其他同步对象时,但是一旦你在另一个调用那些同步对象的方法中调用可能不是),相反,可能只是调用另一个方法的事实可能就足够了使存储在CPU寄存器中的值保存到存储器中。因此,不要求所有变量都是易变的。
此外,我怀疑和其他人也怀疑由于某些线程问题,类的字段没有得到最优化。
一些笔记(我的理解): Thread.MemoryBarrier()主要是一条CPU指令,用于确保写入/读取不会从CPU的角度绕过障碍。 (这与存储在寄存器中的值没有直接关系)所以这可能不是直接导致将变量从寄存器保存到内存的原因(除了根据我们在此讨论的方法调用这一事实,可能会导致这种情况发生) - 它可能真的是任何方法调用,但可能会影响从寄存器中保存的所有类字段)
理论上,JIT / Compiler也可以在同一方法中将该方法转换为帐户,以确保从CPU寄存器中存储变量。但是,按照我们对另一个方法或类的任何调用的简单提议规则,将导致将存储在寄存器中的变量保存到内存中。另外,如果有人用另一种方法包含该调用(可能深入很多方法),编译器就不可能深入分析这种方法来推测执行。 JIT可以做一些事情,但是它可能不会分析那么深,两种情况都需要确保锁定/同步工作,无论如何,因此最简单的优化是可能的答案。
除非我们有任何人编写能够证实这一点的编译器,但它可能是最好的猜测,我们知道为什么不需要volatile。
如果遵循该规则,同步对象只需要在进入和离开时使用自己对MemoryBarrier的调用,以确保CPU具有来自其写入缓存的最新值,以便刷新它们,以便可以读取正确的值。在这个网站上,您将看到隐藏的内存障碍:http://www.albahari.com/threading/part4.aspx
答案 2 :(得分:0)
那么是什么阻止了编译器将_count优化到寄存器中 并且可能只更新锁中的寄存器?
我所知道的文档中没有任何内容可以排除这种情况发生。关键是对Monitor.Exit
的调用将有效地保证_count
的最终值将在完成时提交给内存。
编译器可以知道不将_count优化为a 如果在方法中看到锁定或同步对象,则注册。 但在这种情况下,锁定调用是另一个函数。
从您的角度来看,以其他方法获取和释放锁的事实是无关紧要的。模型内存定义了一组非常严格的规则,必须遵守这些规则关于内存屏障生成器。将这些Monitor
调用放在另一个方法中的唯一结果是JIT编译器将更难以遵守这些规则。但是,JIT编译器必须遵守;期。如果方法调用变得复杂或嵌套得太深,那么我怀疑JIT编译器对这方面可能具有的任何启发式方法进行了支持并说,"忘了它,我只是不打算优化任何东西!& #34;
所以主要的问题是,这是否真的是唯一可行的方法 没有volatile,编译器/ jit不会放入类成员 寄存器中的变量因此是不必要的?
它的工作原理是协议是在读取_count
之前获取锁定。如果读者不这样做,那么所有的赌注都会被取消。