我已经被认为如果多个线程可以访问变量,那么所有对该变量的读取和写入都必须受到同步代码的保护,例如"lock"语句,因为处理器可能会在中途切换到另一个线程写.
但是,我正在使用Reflector查看System.Web.Security.Membership并找到如下代码:
public static class Membership { private static bool s_Initialized = false; private static object s_lock = new object(); private static MembershipProvider s_Provider; public static MembershipProvider Provider { get { Initialize(); return s_Provider; } } private static void Initialize() { if (s_Initialized) return; lock(s_lock) { if (s_Initialized) return; // Perform initialization... s_Initialized = true; } } }
为什么s_Initialized字段在锁外读取?另一个线程难道不能同时写入它吗?变量的读写是否是原子的?
要获得明确的答案,请转到规范.:)
CLI规范第12.6.6节的分区I指出:"符合要求的CLI应保证当对位置的所有写入访问的大小相同时,对不大于本机字大小的正确对齐的内存位置的读写访问权限是原子的. ".
这样就确认了s_Initialized永远不会不稳定,并且对小于32位的原始类型的读写是原子的.
特别是,double
并且long
(Int64
和UInt64
)不保证在32位平台上是原子的.您可以使用Interlocked
类上的方法来保护它们.
此外,虽然读取和写入是原子的,但存在具有加法,减法以及递增和递减基元类型的竞争条件,因为它们必须被读取,操作和重写.互锁类允许您使用CompareExchange
和Increment
方法保护它们.
互锁会创建一个内存屏障,以防止处理器重新排序读取和写入.在此示例中,锁定创建了唯一必需的屏障.
这是双重检查锁定模式的一种(坏)形式,它在C#中不是线程安全的!
这段代码中有一个大问题:
s_Initialized不易变.这意味着初始化代码中的写入可以在s_Initialized设置为true后移动,而其他线程可以看到未初始化的代码,即使s_Initialized为true也是如此.这不适用于Microsoft的Framework实现,因为每次写入都是易失性写入.
但是在Microsoft的实现中,未初始化数据的读取可以重新排序(即由cpu预取),因此如果s_Initialized为true,则读取应该初始化的数据可能导致读取旧的,未初始化的数据,因为缓存命中(即读取被重新排序).
例如:
Thread 1 reads s_Provider (which is null) Thread 2 initializes the data Thread 2 sets s\_Initialized to true Thread 1 reads s\_Initialized (which is true now) Thread 1 uses the previously read Provider and gets a NullReferenceException
在读取s_Initialized之前移动s_Provider的读取是完全合法的,因为在任何地方都没有易失性读取.
如果s_Initialized是volatile,则在读取s_Initialized之前不允许读取s_Provider,并且在s_Initialized设置为true并且现在一切正常后,也不允许提供者的初始化.
Joe Duffy还写了一篇关于这个问题的文章:双重检查锁定的破坏变种
谈谈 - 标题中的问题绝对不是罗里提出的真正问题.
这个名义上的问题有一个简单的答案"不" - 但当你看到真正的问题时,这根本没有任何帮助 - 我认为没有人给出一个简单的答案.
Rory提出的真实问题在很晚才提出,与他给出的例子更为相关.
为什么s_Initialized字段在锁外读取?
对此的答案也很简单,尽管与变量访问的原子性完全无关.
s_Initialized字段在锁外读取,因为锁很昂贵.
由于s_Initialized字段基本上是"一次写入",因此它永远不会返回误报.
在锁外读取它是经济的.
这是一种低成本的具有活性高的有效益的机会.
这就是为什么它在锁外读 - 为了避免支付使用锁的成本,除非它被指出.
如果锁很便宜,代码会更简单,省略第一次检查.
(编辑:来自rory的很好的响应如下.是的,布尔读取是非常原子的.如果有人构建了一个非原子布尔读取的处理器,它们将被添加到DailyWTF上.)
正确的答案似乎是,"是的,主要是."
John引用CLI规范的答案表明,对32位处理器上不大于32位的变量的访问是原子的.
来自C#规范的进一步确认,第5.5节,变量引用的原子性:
以下数据类型的读取和写入是原子的:bool,char,byte,sbyte,short,ushort,uint,int,float和reference类型.此外,在先前列表中具有基础类型的枚举类型的读取和写入也是原子的.其他类型的读写,包括long,ulong,double和decimal,以及用户定义的类型,不保证是原子的.
我的示例中的代码是由ASP.NET团队自己编写的Membership类中的释义,因此可以安全地假设它访问s_Initialized字段的方式是正确的.现在我们知道为什么.
编辑:正如Thomas Danecker指出的那样,即使该字段的访问是原子的,s_Initialized也应该被标记为volatile,以确保处理器重新排序读取和写入时不会破坏锁定.