.NET 中双重检查锁定中需要 volatile 修饰符
多篇文章指出,在 .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;
}
}
}
为什么“lock (syncRoot)”没有实现必要的内存一致性?在“lock”语句之后,读和写都是易失性的,因此可以实现必要的一致性,这不是真的吗?
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:
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?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(8)
挥发性是不必要的。嗯,某种**
易失性
用于在变量的读取和写入之间创建内存屏障*。lock
使用时,除了限制一个线程对该块的访问外,还会导致在lock
内的块周围创建内存屏障。内存屏障使得每个线程读取变量的最新值(而不是缓存在某些寄存器中的本地值),并且编译器不会重新排序语句。使用
易失性
是不必要的**,因为你已经获得了锁。Joseph Albahari 比我更好地解释了这些东西。
请务必查看 Jon Skeet 的在 C# 中实现单例的指南
更新:
*
易失性
导致变量读取为VolatileRead
并写入为VolatileWrite
,这在 CLR 上的 x86 和 x64 上通过以下方式实现一个MemoryBarrier
。它们在其他系统上可能更细粒度。**仅当您在 x86 和 x64 处理器上使用 CLR 时,我的答案才是正确的。在其他内存模型中可能也是如此,例如 Mono(和其他实现)、Itanium64 和未来的硬件。这就是乔恩在他的文章中提到的双重检查锁定的“陷阱”。
可能需要执行{将变量标记为
易失性
、使用Thread.VolatileRead
读取变量或插入对Thread.MemoryBarrier
的调用}之一使代码在弱内存模型情况下正常工作。据我了解,在 CLR 上(甚至在 IA64 上),写入永远不会重新排序(写入始终具有释放语义)。但是,在 IA64 上,读取可能会重新排序为先于写入,除非它们被标记为易失性。不幸的是,我无法访问 IA64 硬件来玩,所以我所说的任何内容都只是猜测。
我还发现这些文章很有帮助:
http://www.codeproject.com/KB/tips/MemoryBarrier.aspx
万斯·莫里森的文章(所有内容都链接到此,它讨论了双重检查锁定)
chris brumme 的文章(所有内容都链接到此)< br>
Joe Duffy:双重检查锁定的损坏变体
luis abreu 的多线程系列也很好地概述了这些概念
http://msmvps。 com/blogs/luisabreu/archive/2009/06/29/multithreading-load-and-store-reordering.aspx
http://msmvps.com/博客/luisabreu/archive/2009/07/03/multithreading-introducing-memory-fences.aspx
Volatile is unnecessary. Well, sort of**
volatile
is 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 thelock
, 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
volatile
is unnecessary** because you've already got a lock.Joseph Albahari explains this stuff way better than I ever could.
And be sure to check out Jon Skeet's guide to implementing the singleton in C#
update:
*
volatile
causes reads of the variable to beVolatileRead
s and writes to beVolatileWrite
s, which on x86 and x64 on CLR, are implemented with aMemoryBarrier
. They may be finer grained on other systems.**my answer is only correct if you are using the CLR on x86 and x64 processors. It might be 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.
Doing one of {marking the variable as
volatile
, reading it withThread.VolatileRead
, or inserting a call toThread.MemoryBarrier
} might be necessary for the code to work properly in a weak memory model situation.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.
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
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
有一种方法可以在没有
volatile
字段的情况下实现它。我会解释一下......我认为锁内的内存访问重新排序是危险的,这样您就可以在锁外获得未完全初始化的实例。为了避免这种情况,我这样做:
理解代码
想象一下,Singleton 类的构造函数中有一些初始化代码。如果在使用新对象的地址设置字段后对这些指令重新排序,那么您将得到一个不完整的实例...想象一下该类具有以下代码:
现在想象一下使用 new 运算符调用构造函数:
这可以是扩展到这些操作:
如果我像这样重新排序这些指令会怎样:
这有什么不同吗?如果您想到单个线程,则否。 是如果你想到多个线程...如果线程在
将实例设置为ptr
之后被中断怎么办:这就是内存屏障通过不允许内存访问来避免的事情重新排序:
快乐编码!
There is a way to implement it without
volatile
field. I'll explain it...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:
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:
Now imagine a call to the constructor using the new operator:
This can be expanded to these operations:
What if I reorder these instructions like this:
Does it make a difference? NO if you think of a single thread. YES if you think of multiple threads... what if the thread is interruped just after
set instance to ptr
:That is what the memory barrier avoids, by not allowing memory access reordering:
Happy coding!
您应该将 volatile 与双重检查锁定模式一起使用。
大多数人都将这篇文章作为你不需要 volatile 的证据:
https://msdn.microsoft.com/en-us/magazine/ cc163715.aspx#S10
但他们未能读到最后:
“最后的警告 - 我只是根据现有处理器上观察到的行为来猜测 x86 内存模型。因此,低锁技术也很脆弱,因为随着时间的推移,硬件和编译器可能会变得更加激进。以下是一些策略首先,尽可能避免低锁技术。最后,尽可能采用最弱的内存模型,使用易失性声明而不是依赖隐式保证。”
如果您需要更有说服力,请阅读这篇有关 ECMA 规范将用于其他平台的文章:
msdn.microsoft.com/en-us/magazine/jj863136.aspx
如果您需要进一步说服,请阅读这篇较新的文章,其中可能会进行优化,以防止它在没有易失性的情况下工作:
msdn.microsoft.com/en-us/magazine/jj883956.aspx
总之,它“可能”暂时为您工作而无需 volatile,但不要冒险它编写正确的代码并使用 volatile 或 volatileread/write 方法。建议采取其他措施的文章有时会忽略 JIT/编译器优化的一些可能的风险,这些风险可能会影响您的代码,以及我们未来可能发生的可能破坏您的代码的优化。另外,正如上一篇文章中提到的假设,之前在没有 易失性的情况下工作的假设可能在 ARM 上不成立。
更多信息请访问:https://essentialsoftwaredevelopment.com
You should use volatile with the double check lock pattern.
Most people point to this article as proof you do not need 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."
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
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
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.
More info in:https://essentialsoftwaredevelopment.com
我认为没有人真正回答过这个问题,所以我会尝试一下。
易失性和第一个
if (instance == null)
不是“必需的”。锁将使这段代码成为线程安全的。所以问题是:为什么要添加第一个
if (instance == null)
?原因大概是为了避免不必要地执行锁定的代码部分。当您在锁内执行代码时,任何其他尝试执行该代码的线程都会被阻止,如果您尝试从多个线程频繁访问单例,这会减慢您的程序速度。根据语言/平台的不同,锁本身也可能会产生您希望避免的开销。
因此,添加第一个空检查是一种非常快速的方法来查看是否需要锁。如果不需要创建单例,则可以完全避免锁。
但是,如果不以某种方式锁定引用,则无法检查引用是否为空,因为由于处理器缓存,另一个线程可能会更改它,并且您将读取“过时”值,这将导致您不必要地输入锁。但你正试图避免锁定!
因此,您可以将单例设置为易失性,以确保您读取最新值,而无需使用锁。
您仍然需要内部锁,因为 volatile 仅在对变量的单次访问期间保护您 - 如果不使用锁,您无法安全地测试和设置它。
现在,这真的有用吗?
好吧,我会说“在大多数情况下,不”。
如果 Singleton.Instance 可能因锁而导致效率低下,那么为什么要如此频繁地调用它以至于这会成为一个严重的问题?单例的要点在于只有一个,因此您的代码可以读取并缓存单例引用一次。
我能想到的唯一无法进行缓存的情况是当您有大量线程时(例如,使用新线程来处理每个请求的服务器可能会创建数百万个运行时间非常短的线程,每个线程这必须调用 Singleton.Instance 一次)。
因此,我怀疑双重检查锁定是一种在非常具体的性能关键案例中占有一席之地的机制,然后每个人都攀上了“这是正确的做法”的潮流,而没有真正考虑它的作用以及它是否有效在他们使用它的情况下实际上是必要的。
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.So the question is: why would you add the first
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.
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.
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.
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).
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.
AFAIK(并且 - 谨慎对待这一点,我没有做很多并发的事情)不。锁只是为您提供多个竞争者(线程)之间的同步。
另一方面,易失性告诉您的机器每次都重新评估该值,这样您就不会偶然发现缓存的(错误的)值。
请参阅 http://msdn.microsoft.com/en-us/library/ms998558 .aspx 并注意以下引用:
易失性的说明: http://msdn.microsoft .com/en-us/library/x13ttww7%28VS.71%29.aspx
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).
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.
See http://msdn.microsoft.com/en-us/library/ms998558.aspx and note the following quote:
A description of volatile: http://msdn.microsoft.com/en-us/library/x13ttww7%28VS.71%29.aspx
锁
就足够了。 MS 语言规范 (3.0) 本身在第 8.12 节中提到了这种确切的场景,但没有提及易失性
:The
lock
is sufficient. The MS language spec (3.0) itself mentions this exact scenario in §8.12, without any mention ofvolatile
:我想我已经找到了我正在寻找的东西。详细信息请参阅本文 - http://msdn.microsoft.com/ en-us/magazine/cc163715.aspx#S10。
综上所述 - 在 .NET 中,在这种情况下确实不需要 volatile 修饰符。然而,在较弱的内存模型中,在延迟启动对象的构造函数中进行的写入可能会在写入字段后延迟,因此其他线程可能会在第一个 if 语句中读取损坏的非空实例。
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.
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.
这是一篇关于使用 volatile 和双重检查锁定的非常好的文章:
http: //tech.puredanger.com/2007/06/15/double-checked-locking/
在 Java 中,如果目标是保护变量,则无需锁定(如果它被标记为 volatile)
This a pretty good post about using volatile with 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