C# 为什么不';t即使在使用监视器时,所有成员变量都需要volatile以确保线程安全?(为什么模型真的有效?)

C# 为什么不';t即使在使用监视器时,所有成员变量都需要volatile以确保线程安全?(为什么模型真的有效?),c#,multithreading,volatile,memory-barriers,C#,Multithreading,Volatile,Memory Barriers,(我知道他们不这样做,但我正在寻找不使用volatile就可以工作的根本原因,因为不应该有任何东西阻止编译器在没有volatile的情况下将变量存储在寄存器中……或者是否存在……) 这个问题源于一种不一致的想法,即如果没有volatile,编译器(理论上可以以各种方式优化任何变量,包括将其存储在CPU寄存器中),而文档称,在使用类似于锁定变量的同步时,不需要这样做。但在某些情况下,编译器/jit似乎无法知道您是否会在代码路径中使用它们。因此,我们怀疑这里确实发生了其他事情,让记忆模型“起作用”

(我知道他们不这样做,但我正在寻找不使用volatile就可以工作的根本原因,因为不应该有任何东西阻止编译器在没有volatile的情况下将变量存储在寄存器中……或者是否存在……)

这个问题源于一种不一致的想法,即如果没有volatile,编译器(理论上可以以各种方式优化任何变量,包括将其存储在CPU寄存器中),而文档称,在使用类似于锁定变量的同步时,不需要这样做。但在某些情况下,编译器/jit似乎无法知道您是否会在代码路径中使用它们。因此,我们怀疑这里确实发生了其他事情,让记忆模型“起作用”

在本例中,是什么阻止编译器/jit优化寄存器中的_计数,从而使增量在寄存器上完成,而不是直接在内存中完成(稍后在exit调用后写入内存)?如果_count是volatile,那么看起来一切都应该很好,但是很多代码都是在没有volatile的情况下编写的。如果编译器在方法中看到锁或同步对象,则可以知道不优化寄存器中的_计数。。但在这种情况下,lock调用位于另一个函数中

大多数文档都说,如果使用像lock这样的同步调用,则不需要使用volatile

那么,是什么阻止编译器优化寄存器中的_计数并可能只更新锁中的寄存器呢?我有一种感觉,因为这个确切的原因,大多数成员变量不会被优化到寄存器中,因为每个成员变量都需要是易失性的,除非编译器能告诉它不应该优化(否则我怀疑大量代码会失败)。我看到一些类似的东西,当C++在几年前,局部函数变量被存储在寄存器中,类成员变量没有。 因此,主要的问题是,编译器/jit不会将类成员变量放入寄存器中,因此volatile就没有必要了,这真的是在没有volatile的情况下工作的唯一方法吗

(请忽略呼叫中缺乏异常处理和安全性,但您了解要点。)


进入和离开
监视器
会导致内存溢出。因此,CLR确保
监视器.Enter
/
监视器.Exit
之前的所有写入操作对所有其他线程都可见,并且方法调用之后的所有读取操作都“发生”在它之后。这也意味着调用前的语句不能在调用后移动,反之亦然


请参阅。

这个问题的最佳猜测答案似乎是,在调用任何函数之前,CPU寄存器中存储的任何变量都会保存到内存中。这是有意义的,因为从单个线程的编译器设计观点来看,需要这样做,否则,如果对象被其他函数/方法/对象使用,则该对象可能看起来不一致。 因此,这可能不像一些人/文章所说的那样,同步对象/类是由编译器检测到的,而非易失性变量是通过调用安全的。(可能是在同一方法中使用锁或其他同步对象时,但一旦您在另一个方法中调用了可能不调用这些同步对象的方法),则很可能仅仅调用另一个方法就足以将存储在CPU寄存器中的值保存到内存中。因此,不要求所有变量都是易变的

此外,我怀疑和其他人也怀疑,由于一些线程问题,类的字段没有得到优化

一些注释(我的理解): MemoryBarrier()主要是一条CPU指令,用于确保从CPU角度看写/读操作不会绕过该障碍。(这与寄存器中存储的值没有直接关系),因此这可能不是直接导致将变量从寄存器保存到内存的原因(除了根据我们在这里的讨论,它是一个方法调用这一事实之外,可能会导致这种情况发生-它可能确实是任何方法调用,但可能会影响从寄存器保存使用的所有类字段)

理论上,JIT/编译器也可以在同一方法中考虑该方法,以确保变量从CPU寄存器中存储。但是,只要遵循我们提出的任何调用另一个方法或类的简单规则,就会将存储在寄存器中的变量保存到内存中。另外,如果有人将该调用打包另一种方法(可能有很多方法)是,编译器不太可能分析那么深的代码来推测执行情况。JIT可以做一些事情,但它也不太可能分析那么深的代码,而且无论发生什么情况,这两种情况都需要确保锁/同步工作,因此最简单的优化就是可能的答案

除非我们有人编写编译器来证实这一点,否则这只是猜测,但这可能是我们对为什么不需要volatile的最好猜测

如果遵循该规则,同步对象只需在进入和离开时调用MemoryBarrier,以确保CPU具有来自其写缓存的最新值,从而刷新这些值,以便可以读取正确的值。在此网站上,您将看到建议的隐式内存障碍:

那么,是什么阻止编译器优化寄存器中的_计数呢 并且可能只更新锁中的寄存器

据我所知,文档中没有任何内容可以阻止这种情况的发生。关键是对
Monitor.Exit
的调用将有效地保证
\u count
的最终值将在完成后提交到内存中

这是有道理的
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
// ...
// ...
}