Kon*_*tin 80 c# singleton 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;
}
}
}
Run Code Online (Sandbox Code Playgroud)
为什么不"锁定(syncRoot)"完成必要的内存一致性?在"lock"语句之后,读取和写入都是不稳定的,因此必须实现必要的一致性,这不是真的吗?
dan*_*dan 59
易失性是不必要的.好吧,有点**
volatile
用于在变量的读写之间创建内存屏障*.
lock
当使用时,lock
除了限制对一个线程的块的访问之外,还会在其内部的块周围创建内存屏障.
内存障碍使得每个线程都读取变量的最新值(不是某个寄存器中缓存的本地值),并且编译器不会重新排序语句.使用volatile
是不必要的**因为你已经锁定了.
Joseph Albahari比以往任何时候都更好地解释了这个问题.
请务必查看Jon Skeet的C#单例实现指南
update:
*volatile
导致变量的读取为VolatileRead
s,写入为VolatileWrite
s,在CL86上的x86和x64上,用a实现MemoryBarrier
.它们可能在其他系统上更精细.
**如果您在x86和x64处理器上使用CLR,我的答案才是正确的.这可能是在其他内存模型真实,就像在Mono(和其他实现),Itanium64和未来的硬件.这就是Jon在他的文章中提到的双重检查锁定的"陷阱".
执行{标记变量volatile
,读取Thread.VolatileRead
或插入调用Thread.MemoryBarrier
}之一可能需要使代码在弱内存模型情况下正常工作.
根据我的理解,在CLR上(甚至在IA64上),写入永远不会被重新排序(写入总是具有释放语义).但是,在IA64上,读取可能会在写入之前重新排序,除非它们标记为volatile.不幸的是,我无法使用IA64硬件,因此我所说的任何内容都是猜测.
我也发现这些文章很有用:
http://www.codeproject.com/KB/tips/MemoryBarrier.aspx
vance morrison的文章(一切都链接到这个,它谈到双重检查锁定)
克里斯布鲁姆的文章 (一切都链接到这个)
Joe Duffy:双重锁定的破坏变种
luis abreu关于多线程的系列文章也很好地概述了这些概念
http://msmvps.com/blogs/luisabreu/archive/2009/06/29/multithreading-load-and-store-reordering.aspx
http:// msmvps. COM /博客/ luisabreu /存档/ 2009/07/03 /多线程引入记忆,fences.aspx
Mig*_*elo 33
有一种方法可以在没有volatile
字段的情况下实现它.我会解释一下......
我认为锁内部的内存访问重新排序是危险的,这样你就可以在锁之外获得一个未完全初始化的实例.为了避免这种情况,我这样做:
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;
}
}
}
Run Code Online (Sandbox Code Playgroud)
想象一下,Singleton类的构造函数中有一些初始化代码.如果在使用新对象的地址设置字段后重新排序这些指令,那么您有一个不完整的实例...想象该类具有以下代码:
private int _value;
public int Value { get { return this._value; } }
private Singleton()
{
this._value = 1;
}
Run Code Online (Sandbox Code Playgroud)
现在想象一下使用new运算符调用构造函数:
instance = new Singleton();
Run Code Online (Sandbox Code Playgroud)
这可以扩展到这些操作:
ptr = allocate memory for Singleton;
set ptr._value to 1;
set Singleton.instance to ptr;
Run Code Online (Sandbox Code Playgroud)
如果我重新排序这些说明怎么办:
ptr = allocate memory for Singleton;
set Singleton.instance to ptr;
set ptr._value to 1;
Run Code Online (Sandbox Code Playgroud)
这有什么不同吗?NO,如果你认为一个单线程的.YES,如果你认为多线程的......如果线程刚过interruped 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
Run Code Online (Sandbox Code Playgroud)
这是内存屏障避免的,不允许内存访问重新排序:
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;
Run Code Online (Sandbox Code Playgroud)
快乐的编码!
我认为没有人真正回答过这个问题,所以我会试一试.
不稳定和第一个if (instance == null)
不是"必要的".锁将使此代码线程安全.
所以问题是:你为什么要加第一个if (instance == null)
呢?
原因可能是避免不必要地执行锁定的代码段.当您在锁内部执行代码时,任何其他尝试执行该代码的线程都会被阻止,如果您尝试从多个线程中频繁访问该单例,这将减慢您的程序速度.根据语言/平台的不同,锁定本身也可能存在您希望避免的开销.
因此,添加第一个空检查是一种非常快速的方法,可以查看是否需要锁定.如果您不需要创建单例,则可以完全避免锁定.
但是你无法检查引用是否为null而不以某种方式锁定它,因为由于处理器缓存,另一个线程可能会更改它,你会读到一个"陈旧"值,导致你不必要地进入锁.但你正试图避免锁定!
因此,您可以使单例易失性,以确保您读取最新值,而无需使用锁定.
您仍然需要内部锁定,因为volatile仅在单次访问变量时保护您 - 您无法在不使用锁定的情况下安全地进行测试和设置.
现在,这实际上有用吗?
好吧,我会说"在大多数情况下,没有".
如果Singleton.Instance可能因锁而导致效率低下,那么为什么要频繁调用它以至于这将是一个重大问题呢?单例的重点是只有一个,所以你的代码可以读取和缓存单例引用一次.
我能想到这种缓存不可能的唯一情况是当你有大量的线程时(例如,使用新线程来处理每个请求的服务器可能会创建数百万个非常短的运行线程,每个线程都是这将不得不调用Singleton.Instance一次).
因此,我怀疑双重检查锁定是一种在非常具体的性能关键情况下具有真实位置的机制,然后每个人都已经抓住了"这是正确的方式来做到这一点"并没有真正想到它做什么以及是否在他们使用它的情况下实际上是必要的.
您应该将 volatile 与双重检查锁定模式一起使用。
大多数人指出这篇文章证明你不需要 volatile:https : //msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10
但他们没有读到最后:“最后一句警告——我只是从现有处理器上观察到的行为中猜测 x86 内存模型。因此,低锁定技术也很脆弱,因为硬件和编译器会随着时间的推移变得更加积极. 这里有一些策略可以最大限度地减少这种脆弱性对您的代码的影响。首先,尽可能避免使用低锁技术。(...) 最后,假设可能的最弱内存模型,使用 volatile 声明而不是依赖隐式保证.”
如果您需要更有说服力,请阅读有关将用于其他平台的 ECMA 规范的这篇文章:msdn.microsoft.com/en-us/magazine/jj863136.aspx
如果您需要进一步令人信服,请阅读这篇较新的文章,其中可能会进行优化以防止它在没有 volatile 的情况下工作:msdn.microsoft.com/en-us/magazine/jj883956.aspx
总之,它“可能”在没有 volatile 的情况下为您工作,但不要冒险编写正确的代码并使用 volatile 或 volatile 读/写方法。建议不这样做的文章有时会遗漏一些可能影响您的代码的 JIT/编译器优化的可能风险,以及我们未来可能发生的可能会破坏您的代码的优化。同样如上一篇文章中提到的假设,之前在没有 volatile 的情况下工作的假设可能不适用于 ARM。
归档时间: |
|
查看次数: |
24505 次 |
最近记录: |