为什么即使使用 Monitor,也不是所有成员变量都需要 volatile 来保证线程安全?(为什么模型真的有效?) [英] Why don't all member variables need volatile for thread safety even when using Monitor? (why does the model really work?)

查看:55
本文介绍了为什么即使使用 Monitor,也不是所有成员变量都需要 volatile 来保证线程安全?(为什么模型真的有效?)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

(我知道他们没有,但我正在寻找这实际上在不使用 volatile 的情况下工作的根本原因,因为应该没有什么阻止编译器在没有 volatile 的情况下将变量存储在寄存器中......或者在那里.......)

(I know they don't but I'm looking for the underlying reason this actually works without using volatile since there should be nothing preventing the compiler from storing a variable in a register without volatile... or is there...)

这个问题源于一种不和谐的想法,即没有 volatile 编译器(理论上可以以各种方式优化任何变量,包括将其存储在 CPU 寄存器中.)虽然文档说在使用同步时不需要像锁定变量一样.但是在某些情况下,编译器/jit 似乎无法知道您是否会在代码路径中使用它们.所以怀疑这里确实发生了其他事情以使内存模型工作".

This question stems from the discord of thought that without volatile the compiler (can optimize in theory any variable in various ways including storing it in a CPU register.) While the docs say that is not needed when using synchronization like lock around variables. But there is really in some cases seemingly no way the compiler/jit can know you will use them or not in your code path. So the suspicion is something else is really happening here to make the memory model "work".

在这个例子中,是什么阻止了编译器/jit 将 _count 优化到寄存器中,从而在寄存器上完成增量而不是直接到内存(稍后在退出调用后写入内存)?如果 _count 是 volatile 似乎一切都应该没问题,但是很多代码是在没有 volatile 的情况下编写的.如果编译器在方法中看到锁或同步对象,它可能知道不将 _count 优化到寄存器中是有道理的.但在这种情况下,锁调用在另一个函数中.

In this example what prevents the compiler/jit from optimizing _count into a register and thus having the increment done on the register rather then directly to memory (later writing to memory after the exit call)? If _count were volatile it would seem everything should be fine, but a lot of code is written without volatile. It makes sense the compiler could know not to optimize _count into a register if it saw a lock or synchronization object in the method.. but in this case the lock call is in another function.

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

Most documentation says you don't need to use volatile if you use a synchronization call like lock.

那么是什么阻止编译器将 _count 优化到一个寄存器中并可能只更新锁内的寄存器呢?我有一种感觉,由于这个确切的原因,大多数成员变量不会被优化到寄存器中,因为除非编译器可以告诉它不应该优化(否则我怀疑大量代码会失败),否则每个成员变量都确实需要可变.几年前我在看 C++ 时看到了类似的东西,局部函数变量被存储在寄存器中,而类成员变量没有.

So what prevents the compiler from optimizing _count into a register and potentially updating just the register within the lock? I have a feeling that most member variables won't be optimized into registers for this exact reason as then every member variable would really need to be volatile unless the compiler could tell it shouldn't optimize (otherwise I suspect tons of code would fail). I saw something similar when looking at C++ years ago local function variables got stored in registers, class member variables did not.

所以主要的问题是,编译器/jit 不会将类成员变量放在寄存器中,因此在没有 volatile 的情况下,这是否真的是唯一可行的方法,因此 volatile 是不必要的?

So the main question is, is it really the only way this possibly works without volatile that the compiler/jit won't put class member variables in registers and thus volatile is then unnecessary?

(请忽略调用中缺乏异常处理和安全性的问题,但你明白了要点.)

(Please ignore the lack of exception handling and safety in the calls, but you get the gist.)

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
// ...
// ...
}

推荐答案

这个问题的最佳答案似乎是存储在 CPU 寄存器中的任何变量都在调用任何函数之前保存到内存中.这是有道理的,因为从单个线程的编译器设计观点需要这样做,否则如果对象被其他函数/方法/对象使用,它可能看起来不一致.因此,它可能不像某些人/文章声称编译器检测到同步对象/类并且通过它们的调用使非易失性变量安全.(也许它们是在同一方法中使用锁或其他同步对象时,但是一旦您在另一个调用这些同步对象的方法中调用可能不会),相反,可能仅调用另一个方法的事实可能就足够了使存储在 CPU 寄存器中的值保存到内存中.因此不需要所有变量都是可变的.

The best guess answer to this question would appear to be that that any variables that are stored in CPU registers are saved to memory before any function would be called. This makes sense because compiler design viewpoint from a single thread would require that, otherwise the object might appear to be inconsistent if it were used by other functions/methods/objects. So it may not be so much as some people/articles claim that synchronization objects/classes are detected by the compilers and non-volatile variables are made safe through their calls. (Perhaps they are when a lock is used or other synchronization objects in the same method, but once you have calls in another method that calls those synchronization objects probably not), instead it is likely that just the fact of calling another method is probably enough to cause the values stored in CPU registers to be saved to memory. Thus not requiring all variables to be volatile.

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

Also I suspect and others have suspected too that fields of a class are not optimized as much due to some of the threading concerns.

一些注意事项(我的理解):从 CPU 的角度来看,Thread.MemoryBarrier() 主要是一条 CPU 指令,以确保写入/读取不会绕过屏障.(这与存储在寄存器中的值没有直接关系)所以这可能不是直接导致将变量从寄存器保存到内存的原因(除了根据我们在这里的讨论它是一个方法调用,可能会导致这种情况发生- 它可能真的是任何方法调用,尽管可能会影响从寄存器中保存的所有类字段)

Some notes (my understanding): Thread.MemoryBarrier() is mostly a CPU instruction to insure writes/reads don't bypass the barrier from a CPU perspective. (This is not directly related to values stored in registers) So this is probably not what directly causes to save variables from registers to memory (except just by the fact it is a method call as per our discussion here, would likely cause that to happen- It could have really been any method call though perhaps to affect all class fields that were used being saved from registers)

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

It is theoretically possible the JIT/Compiler could also take that method into an account in the same method to ensure variables are stored from CPU registers. But just following our simple proposed rule of any calls to another method or class would result in saving variables stored in registers to memory. Plus if someone wrapped that call in another method (maybe many methods deep), the compiler wouldn't likely analyze that deep to speculate on execution. The JIT could do something but again it likely wouldn't analyze that deep, and both cases need to ensure locks/synchronization work no matter what, thus the simplest optimization is the likely answer.

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

Unless we have anyone that writes the compilers that can confirm this its all a guess, but its likely the best guess we have of why volatile is not needed.

如果遵循该规则,同步对象只需要在它们进入和离开时使用自己对 MemoryBarrier 的调用,以确保 CPU 具有其写入缓存中的最新值,以便刷新它们以便可以读取正确的值.在此站点上,您将看到建议的隐式内存屏障:http://www.albahari.com/threading/part4.aspx

If that rule is followed synchronization objects just need to employ their own call to MemoryBarrier when they enter and leave to ensure the CPU has the most up to date values from its write caches so they get flushed so proper values can be read. On this site you will see that is what is suggested implicit memory barriers: http://www.albahari.com/threading/part4.aspx

这篇关于为什么即使使用 Monitor,也不是所有成员变量都需要 volatile 来保证线程安全?(为什么模型真的有效?)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆