C# .NET 中双重检查锁定中需要 volatile 修饰符

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1964731/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-06 22:15:25  来源:igfitidea点击:

The need for volatile modifier in double checked locking in .NET

c#singletonvolatile

提问by Konstantin

Multiple texts say that when implementing double-checked locking in .NET the field you are locking on should have volatile modifier applied. But why exactly? Considering the following example:

多篇文章说,在 .NET 中实现双重检查锁定时,您锁定的字段应该应用 volatile 修饰符。但究竟是为什么呢?考虑以下示例:

public sealed class Singleton
{
   private static volatile Singleton instance;
   private static object syncRoot = new Object();

   private Singleton() {}

   public static Singleton Instance
   {
      get 
      {
         if (instance == null) 
         {
            lock (syncRoot) 
            {
               if (instance == null) 
                  instance = new Singleton();
            }
         }

         return instance;
      }
   }
}

why doesn't "lock (syncRoot)" accomplish the necessary memory consistency? Isn't it true that after "lock" statement both read and write would be volatile and so the necessary consistency would be accomplished?

为什么“锁定(syncRoot)”不能实现必要的内存一致性?在“锁定”语句之后,读和写都是不稳定的,因此可以实现必要的一致性,这不是真的吗?

采纳答案by dan

Volatile is unnecessary. Well, sort of**

挥发性是不必要的。嗯,有点**

volatileis used to create a memory barrier* between reads and writes on the variable.
lock, when used, causes memory barriers to be created around the block inside the lock, in addition to limiting access to the block to one thread.
Memory barriers make it so each thread reads the most current value of the variable (not a local value cached in some register) and that the compiler doesn't reorder statements. Using volatileis unnecessary** because you've already got a lock.

volatile用于在读取和写入变量之间创建内存屏障*。
lock,当使用时,lock除了将对该块的访问限制为一个线程之外,还会导致在内部的块周围创建内存屏障。
内存屏障使得每个线程读取变量的最新值(不是缓存在某个寄存器中的本地值)并且编译器不会重新排序语句。使用volatile是不必要的**,因为您已经获得了锁。

Joseph Albahariexplains this stuff way better than I ever could.

约瑟夫·阿尔巴哈里 (Joseph Albahari)比我以往任何时候都更好地解释了这些东西。

And be sure to check out Jon Skeet's guide to implementing the singletonin C#

并且一定要查看 Jon Skeet在 C# 中实现单例指南


update:
*volatilecauses reads of the variable to be VolatileReads and writes to be VolatileWrites, which on x86 and x64 on CLR, are implemented with a MemoryBarrier. They may be finer grained on other systems.


update:
*volatile导致变量的读取为VolatileReads,写入为VolatileWrites,这在 x86 和 x64 上的 CLR 上使用MemoryBarrier. 它们在其他系统上可能更细粒度。

**my answer is only correct if you are using the CLR on x86 and x64 processors. It mightbe true in other memory models, like on Mono (and other implementations), Itanium64 and future hardware. This is what Jon is referring to in his article in the "gotchas" for double checked locking.

**我的回答只有在 x86 和 x64 处理器上使用 CLR 时才是正确的。这可能适用于其他内存模型,例如 Mono(和其他实现)、Itanium64 和未来的硬件。这就是 Jon 在他的关于双重检查锁定的“陷阱”中的文章中所指的内容。

Doing one of {marking the variable as volatile, reading it with Thread.VolatileRead, or inserting a call to Thread.MemoryBarrier} might be necessary for the code to work properly in a weak memory model situation.

执行 {marking the variable as volatile,用 读取它Thread.VolatileRead或插入对Thread.MemoryBarrier}的调用之一可能是代码在弱内存模型情况下正常工作所必需的。

From what I understand, on the CLR (even on IA64), writes are never reordered (writes always have release semantics). However, on IA64, reads may be reordered to come before writes, unless they are marked volatile. Unfortuantely, I do not have access to IA64 hardware to play with, so anything I say about it would be speculation.

据我了解,在 CLR 上(甚至在 IA64 上),写入永远不会重新排序(写入始终具有发布语义)。但是,在 IA64 上,读取可能会在写入之前重新排序,除非它们被标记为 volatile。不幸的是,我无法使用 IA64 硬件来玩,所以我所说的任何关于它的都是猜测。

i've also found these articles helpful:
http://www.codeproject.com/KB/tips/MemoryBarrier.aspx
vance morrison's article(everything links to this, it talks about double checked locking)
chris brumme's article(everything links to this)
Joe Duffy: Broken Variants of Double Checked Locking

我还发现这些文章很有帮助:
http: //www.codeproject.com/KB/tips/MemoryBarrier.aspx
vance morrison 的文章(所有链接到此,它谈到双重检查锁定)
chris brumme 的文章(所有链接到此)
Joe Duffy:双重检查锁定的破碎变体

luis abreu's series on multithreading give a nice overview of the concepts too
http://msmvps.com/blogs/luisabreu/archive/2009/06/29/multithreading-load-and-store-reordering.aspx
http://msmvps.com/blogs/luisabreu/archive/2009/07/03/multithreading-introducing-memory-fences.aspx

luis abreu 的多线程系列也很好地概述了这些概念
http://msmvps.com/blogs/luisabreu/archive/2009/06/29/multithreading-load-and-store-reordering.aspx
http://msmvps。 com/blogs/luisabreu/archive/2009/07/03/multithreading-introducing-memory-fences.aspx

回答by Mark Pope

This a pretty good post about using volatile with double checked locking:

这是一篇关于使用 volatile 和双重检查锁定的很好的帖子:

http://tech.puredanger.com/2007/06/15/double-checked-locking/

http://tech.puredanger.com/2007/06/15/double-checked-locking/

In Java, if the aim is to protect a variable you don't need to lock if it's marked as volatile

在 Java 中,如果目的是保护一个变量,如果它被标记为 volatile,则不需要锁定

回答by Benjamin Podszun

AFAIK (and - take this with caution, I'm not doing a lot of concurrent stuff) no. The lock just gives you synchronization between multiple contenders (threads).

AFAIK(并且 - 谨慎对待,我没有做很多并发的事情)不。锁只是让你在多个竞争者(线程)之间进行同步。

volatile on the other hand tells your machine to reevaluate the value every time, so that you don't stumble upon a cached (and wrong) value.

另一方面, volatile 告诉您的机器每次都重新评估该值,这样您就不会偶然发现缓存的(和错误的)值。

See http://msdn.microsoft.com/en-us/library/ms998558.aspxand note the following quote:

请参阅http://msdn.microsoft.com/en-us/library/ms998558.aspx并注意以下引用:

Also, the variable is declared to be volatile to ensure that assignment to the instance variable completes before the instance variable can be accessed.

此外,变量被声明为 volatile 以确保在可以访问实例变量之前完成对实例变量的赋值。

A description of volatile: http://msdn.microsoft.com/en-us/library/x13ttww7%28VS.71%29.aspx

volatile 的描述:http: //msdn.microsoft.com/en-us/library/x13ttww7%28VS.71%29.aspx

回答by Jason Williams

I don't think anybody has actually answered the question, so I'll give it a try.

我认为没有人真正回答过这个问题,所以我会试一试。

The volatile and the first if (instance == null)are not "necessary". The lock will make this code thread-safe.

volatile 和第一个if (instance == null)不是“必需的”。锁将使此代码成为线程安全的。

So the question is: why would you add the first if (instance == null)?

所以问题是:为什么要添加第一个if (instance == null)

The reason is presumably to avoid executing the locked section of code unnecessarily. While you are executing the code inside the lock, any other thread that tries to also execute that code is blocked, which will slow your program down if you try to access the singleton frequently from many threads. Depending on the language/platform, there could also be overheads from the lock itself that you wish to avoid.

原因大概是为了避免不必要地执行锁定的代码部分。当您在锁中执行代码时,任何其他尝试执行该代码的线程都会被阻塞,如果您尝试从多个线程频繁访问单例,这将减慢您的程序速度。根据语言/平台的不同,您还希望避免来自锁本身的开销。

So the first null check is added as a really quick way to see if you need the lock. If you don't need to create the singleton, you can avoid the lock entirely.

因此,添加第一个空检查是一种非常快速的方式来查看您是否需要锁定。如果不需要创建单例,则可以完全避免锁定。

But you can't check if the reference is null without locking it in some way, because due to processor caching, another thread could change it and you would read a "stale" value that would lead you to enter the lock unnecessarily. But you're trying to avoid a lock!

但是您不能在不以某种方式锁定它的情况下检查该引用是否为空,因为由于处理器缓存,另一个线程可能会更改它,并且您将读取一个“陈旧”值,这将导致您不必要地输入锁定。但是你试图避免锁定!

So you make the singleton volatile to ensure that you read the latest value, without needing to use a lock.

因此,您将单例设为 volatile 以确保您读取最新值,而无需使用锁。

You still need the inner lock because volatile only protects you during a single access to the variable - you can't test-and-set it safely without using a lock.

您仍然需要内部锁,因为 volatile 仅在对变量的单次访问期间保护您 - 您无法在不使用锁的情况下安全地测试和设置它。

Now, is this actually useful?

现在,这真的有用吗?

Well I would say "in most cases, no".

好吧,我会说“在大多数情况下,不”。

If Singleton.Instance could cause inefficiency due to the locks, then why are you calling it so frequently that this would be a significant problem? The whole point of a singleton is that there is only one, so your code can read and cache the singleton reference once.

如果 Singleton.Instance 可能因锁定而导致效率低下,那么为什么您如此频繁地调用它以至于这将是一个重大问题?单例的全部意义在于只有一个,因此您的代码可以一次读取和缓存单例引用。

The only case I can think of where this caching wouldn't be possible would be when you have a large number of threads (e.g. a server using a new thread to process every request could be creating millions of very short-running threads, each of which would have to call Singleton.Instance once).

我能想到的唯一情况是当您有大量线程时(例如,使用新线程处理每个请求的服务器可能会创建数百万个非常短的运行线程,每个线程这将不得不调用 Singleton.Instance 一次)。

So I suspect that double checked locking is a mechanism that has a real place in very specific performance-critical cases, and then everybody has clambered on the "this is the proper way to do it" bandwagon without actually thinking what it does and whether it will actually be necessary in the case they are using it for.

所以我怀疑双重检查锁定是一种在非常特定的性能关键情况下真正占有一席之地的机制,然后每个人都攀上了“这是正确的做法”的潮流,而没有真正考虑它的作用以及它是否在他们使用它的情况下实际上是必要的。

回答by Marc Gravell

The lockis sufficient. The MS language spec (3.0) itself mentions this exact scenario in §8.12, without any mention of volatile:

lock是足够了。MS 语言规范 (3.0) 本身在 §8.12 中提到了这个确切的场景,但没有提到volatile

A better approach is to synchronize access to static data by locking a private static object. For example:

class Cache
{
    private static object synchronizationObject = new object();
    public static void Add(object x) {
        lock (Cache.synchronizationObject) {
          ...
        }
    }
    public static void Remove(object x) {
        lock (Cache.synchronizationObject) {
          ...
        }
    }
}

更好的方法是通过锁定私有静态对象来同步对静态数据的访问。例如:

class Cache
{
    private static object synchronizationObject = new object();
    public static void Add(object x) {
        lock (Cache.synchronizationObject) {
          ...
        }
    }
    public static void Remove(object x) {
        lock (Cache.synchronizationObject) {
          ...
        }
    }
}

回答by Konstantin

I think that I've found what I was looking for. Details are in this article - http://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10.

我想我已经找到了我要找的东西。详细信息在本文中 - http://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10

To sum up - in .NET volatile modifier is indeed not needed in this situation. However in weaker memory models writes made in constructor of lazily initiated object may be delayed after write to the field, so other threads might read corrupt non-null instance in the first if statement.

总而言之 - 在这种情况下确实不需要 .NET volatile 修饰符。然而,在较弱的内存模型中,在写入字段后延迟启动对象的构造函数中的写入可能会延迟,因此其他线程可能会在第一个 if 语句中读取损坏的非空实例。

回答by Miguel Angelo

There is a way to implement it without volatilefield. I'll explain it...

有一种方法可以在没有volatile字段的情况下实现它。我来解释一下...

I think that it is memory access reordering inside the lock that is dangerous, such that you can get a not completelly initialized instance outside of the lock. To avoid this I do this:

我认为在锁内重新排序内存访问是危险的,这样您就可以在锁外获得一个未完全初始化的实例。为了避免这种情况,我这样做:

public sealed class Singleton
{
   private static Singleton instance;
   private static object syncRoot = new Object();

   private Singleton() {}

   public static Singleton Instance
   {
      get 
      {
         // very fast test, without implicit memory barriers or locks
         if (instance == null)
         {
            lock (syncRoot)
            {
               if (instance == null)
               {
                    var temp = new Singleton();

                    // ensures that the instance is well initialized,
                    // and only then, it assigns the static variable.
                    System.Threading.Thread.MemoryBarrier();
                    instance = temp;
               }
            }
         }

         return instance;
      }
   }
}

Understanding the code

理解代码

Imagine that there are some initialization code inside the constructor of the Singleton class. If these instructions are reordered after the field is set with the address of the new object, then you have an incomplete instance... imagine that the class has this code:

想象一下,Singleton 类的构造函数内部有一些初始化代码。如果这些指令在用新对象的地址设置字段后重新排序,那么你有一个不完整的实例......想象这个类有这个代码:

private int _value;
public int Value { get { return this._value; } }

private Singleton()
{
    this._value = 1;
}

Now imagine a call to the constructor using the new operator:

现在想象一下使用 new 运算符调用构造函数:

instance = new Singleton();

This can be expanded to these operations:

这可以扩展为以下操作:

ptr = allocate memory for Singleton;
set ptr._value to 1;
set Singleton.instance to ptr;

What if I reorder these instructions like this:

如果我像这样重新排列这些指令会怎样:

ptr = allocate memory for Singleton;
set Singleton.instance to ptr;
set ptr._value to 1;

Does it make a difference? NOif you think of a single thread. YESif you think of multiple threads... what if the thread is interruped just after set instance to ptr:

这有什么不同吗?不,如果你想到一个单一的线程。是的,如果您考虑多个线程......如果线程在以下之后被中断怎么办set instance to ptr

ptr = allocate memory for Singleton;
set Singleton.instance to ptr;
-- thread interruped here, this can happen inside a lock --
set ptr._value to 1; -- Singleton.instance is not completelly initialized

That is what the memory barrier avoids, by not allowing memory access reordering:

这就是内存屏障通过不允许内存访问重新排序来避免的:

ptr = allocate memory for Singleton;
set temp to ptr; // temp is a local variable (that is important)
set ptr._value to 1;
-- memory barrier... cannot reorder writes after this point, or reads before it --
-- Singleton.instance is still null --
set Singleton.instance to temp;

Happy coding!

快乐编码!

回答by user2685937

You should use volatile with the double check lock pattern.

您应该将 volatile 与双重检查锁定模式一起使用。

Most people point to this article as proof you do not need volatile: https://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10

大多数人指出这篇文章证明你不需要 volatile:https: //msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10

But they fail to read to the end: "A Final Word of Warning - I am only guessing at the x86 memory model from observed behavior on existing processors. Thus low-lock techniques are also fragile because hardware and compilers can get more aggressive over time. Here are some strategies to minimize the impact of this fragility on your code. First, whenever possible, avoid low-lock techniques. (...) Finally, assume the weakest memory model possible, using volatile declarations instead of relying on implicit guarantees."

但他们没有读到最后:“最后一句警告——我只是从现有处理器上观察到的行为中猜测 x86 内存模型。因此,低锁定技术也很脆弱,因为硬件和编译器会随着时间的推移变得更加积极. 这里有一些策略可以最大限度地减少这种脆弱性对您的代码的影响。首先,尽可能避免使用低锁技术。(...) 最后,假设可能的最弱内存模型,使用 volatile 声明而不是依赖隐式保证.”

If you need more convincing then read this article on the ECMA spec will be used for other platforms: msdn.microsoft.com/en-us/magazine/jj863136.aspx

如果您需要更有说服力,请阅读有关将用于其他平台的 ECMA 规范的这篇文章:msdn.microsoft.com/en-us/magazine/jj863136.aspx

If you need further convincing read this newer article that optimizations may be put in that prevent it from working without volatile: msdn.microsoft.com/en-us/magazine/jj883956.aspx

如果您需要进一步令人信服,请阅读这篇较新的文章,其中可能会进行优化以防止它在没有 volatile 的情况下工作:msdn.microsoft.com/en-us/magazine/jj883956.aspx

In summary it "might" work for you without volatile for the moment, but don't chance it write proper code and either use volatile or the volatileread/write methods. Articles that suggest to do otherwise are sometimes leaving out some of the possible risks of JIT/compiler optimizations that could impact your code, as well us future optimizations that may happen that could break your code. Also as mentioned assumptions in the last article previous assumptions of working without volatile already may not hold on ARM.

总之,它“可能”在没有 volatile 的情况下为您工作,但不要冒险编写正确的代码并使用 volatile 或 volatile 读/写方法。建议不这样做的文章有时会遗漏一些可能影响您的代码的 JIT/编译器优化的可能风险,以及我们未来可能发生的可能会破坏您的代码的优化。同样如上一篇文章中提到的假设,之前在没有 volatile 的情况下工作的假设可能不适用于 ARM。