题
我已经提出了相信,如果多线程可以访问的一个变量,然后都读和写这个变量都必须受到保护的同步码,如"锁定"的声明,因为处理器可能会切换到另一个线程的过程中,编写。
然而,我一直在寻找通过系统。网。安全。会员国利用反射器和发现这样的代码:
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场读之外的锁?不另外一个线程,以试图写信给它在同一时间? 是读和写的变量原子?
解决方案
为明确的答案走向规范。:)
分区节12.6.6的CLI规范国家:"一个符合CLI应当保证,读和写的访问正确地对准存储器的位置没有大于字母的大小是原子当所有的写访问的位置是相同的大小。"
因此,确认s_Initialized将永远是不稳定的,即阅读和写primitve类型小于32位是原子.
特别是, double
和 long
(Int64
和 UInt64
)是 不 保证原子上的一个32位的平台。你可以使用的方法 Interlocked
类保护这些。
此外,读和写的都是原子,有一个比赛的条件下与另外,减,并增加和减少的原始种类型,因为它们必须阅读、操作和改写。联锁类可以让你保护这些使用 CompareExchange
和 Increment
方法。
锁创建一个记忆屏障,以防止处理器从重新排序的读和写。锁创造所需的唯一障碍,在这个例子。
其他提示
这是一个(糟糕)构成的双重检查锁定模式,这是 不 线的安全C#!
有一个很大的问题在这个代号:
s_Initialized是不易挥发。这意味着写道,在初始化码可移动之后s_Initialized设置为真正的和其他的线可以看到的初始化码即使s_Initialized是真实的。这不适用于微软的执行情况的框架,因为每一个编写是一个不稳定的编写。
而且在微软的执行情况,读取的初始化的数据可以重新排序(即预取的cpu),因此,如果s_Initialized是真的,读取的数据,应该初始化可能会导致读旧的、未初始化的数据,因为高速缓冲击(ie。读重新排序).
例如:
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_Provider之前读的s_Initialized是完全合法的,因为没有挥发性读任何地方。
如果s_Initialized将会挥发、读的s_Provider不会被允许移动之前读的s_Initialized和也初始化的供应商是不允许的举动之后s_Initialized设置为真正的一切都是好的现在。
乔达菲,也写了一篇关于这个问题: 破裂变种双重检查锁定
挂有关--的问题是在标题是绝对不真正的问题,Rory要求。
名义上的问题都有简单的答复"否"--但这并没有帮助在所有的,当你看到真正的问题--我不认为有人已经给出一个简单的答案。
真正的问题罗里要求提交得很晚和更加相关的例子,他给。
为什么是s_Initialized场读 外部的锁?
在回答这也是简单的,但完全无关的原子性的变量访问。
该s_Initialized领域是阅读之外的锁,因为 锁是昂贵的.
由于s_Initialized领域基本上是"一次编写"它将永远不会返回一个虚假的积极的。
这是经济读它外锁。
这是一个 低成本 活动 高 机会有益处。
这就是为什么它的阅读之外的锁--以避免支付的费用使用锁,除非它是指示。
如果锁定了廉价的代码将更简单,省略这一检查。
(编辑:好的响应,从rory下。叶,布尔读的是很多的原子.如果有人建立一个处理器非原子读布尔,他们会登上DailyWTF.)
正确的答案似乎是,"是的。"
- 约翰回答引用CLI规格表明,访问量的变量不大于32位的32位的处理器是原子.
进一步确认从C#规范,第5.5节 原子性的变量的参考文献:
读和写的下列数据类型的原子:bool,char、字符号字节整数uint,int、浮动,并参照类型。此外,读和写的枚举的类型与一个潜在的类型在上一清单也是原子的。读和写的其他类型,包括长期的,无符长,双和小,以及作为用户定义的类型,不能保证原子.
代码在我的例子是转述从会员国类,作为书面通过ASP.NET 团队自己,因此它始终是安全的假设,它的方式访问s_Initialized场是正确的。现在我们知道为什么。
编辑:正如托马斯*Danecker指出,尽管所访问的场是原子的,s_Initialized真的应该是标记 易失性 确认锁定不断通过处理重新排序的读和写。
初始化功能是错误的。它看起来应该更多这样的:
private static void Initialize()
{
if(s_initialized)
return;
lock(s_lock)
{
if(s_Initialized)
return;
s_Initialized = true;
}
}
没有第二次检查,内部的锁定它是可能的初始代码将执行两次。所以第一个检查是为性能救你把锁的不必要的,第二次检查是对的情况下一个线程正在执行的初始代码,但还没有设置 s_Initialized
船旗国和以第二线会通过的第一个检查并等待在锁。
读和写的变量是没有原子的。你需要使用同步Api模仿原子读写。
对于一个很棒的参照关于这个和许多问题并发,确保你抓住一个复制的乔达菲的 最新的奇观.这是一个开膛手!
"访问的一个变量在C#一个原子的行动?"
不。这不是一个C#东西,也不是它甚至一个。净的东西,这是一个处理的事情。
OJ是乔达菲的家伙要去为这类信息。和"联锁"是一个伟大的搜索词的使用,如果你想知道更多。
"撕读"可以发生在任何价值的领域加起来超过大小的指针。
@莱昂
我明白你的意思-我们已经问,然后作了评论,问题可以采取几种不同的方式。
到是清楚的,我想知道如果这是安全的并行线阅读和写信给布尔现场没有任何明确的步码,即,正在访问布尔(或者其他原型)可变原子.
然后我用的成员身份代码,以举一个具体的例子,但这引进了一堆杂念,如双检查锁,事实上,s_Initialized是只有过一次和我说了初始化时代码本身。
我不良。
你也可以装饰s_Initialized与挥发性的关键词和放弃使用的锁。
这是不正确的。你仍然会遇到问题的第二线通过之前,检查第一线有机会设置标志,这将导致在多个执行的初始代码。
我认为你是问 s_Initialized
可能是在一个不稳定的状态的时候读的外锁。简短的回答是没有。一个简单的分配/读将归结到一个单一的大会指令,这是原子上的处理器每个我能想到的。
我不确定什么情况下被分配到64位变量,这取决于处理器,我们认为,它不是原子但它可能是现代的32位的处理器一定在所有64位处理器。分配的复杂值的类型不会是原子的。
我以为他们-我不知道点的锁在你的例子除非你还在做的事情s_Provider在同一时间-然后锁定将确保这些呼吁发生了一起。
这不会 //Perform initialization
注释涵盖创建s_Provider?例如
private static void Initialize()
{
if (s_Initialized)
return;
lock(s_lock)
{
s_Provider = new MembershipProvider ( ... )
s_Initialized = true;
}
}
否则,静态的酒店得到的只是要回null无论如何。
让你的代码一直工作上的弱排序的架构,必须把MemoryBarrier之前你写s_Initialized.
s_Provider = new MemershipProvider;
// MUST PUT BARRIER HERE to make sure the memory writes from the assignment
// and the constructor have been wriitten to memory
// BEFORE the write to s_Initialized!
Thread.MemoryBarrier();
// Now that we've guaranteed that the writes above
// will be globally first, set the flag
s_Initialized = true;
存储器中写道,发生在数的构造和写信给s_Provider都不能保证会发生在你之前写信给s_Initialized上的弱有序的处理器。
很多想法在这个线程有关的东西是否是原子或没有。这不是问题。问题是 所以,你的螺纹的写的是看到其他线.在弱排序架构,写入存储器不会发生在了这就是真正的问题不是是否一变量内的数据的巴士。
编辑: 实际上,我是混合的平台,在我的发言。C#CLR规范的要求写入全球可见,在订(通过使用昂贵的存储指令,对于每一个商店,如果必要)。因此,你不需要实际上有记忆屏障。但是,如果它们C或C++在没有这种保证的全球知名度以存在和目标的平台,可能有弱序存储器,这是多线程,然后你会需要确保构造写的全球可见之前更新s_Initialized,这是测试之外锁。
一个 If (itisso) {
检查布尔是原子,但即使它不是
没有必要锁的第一次检查。
如果任何线已经完成的初始化,那么这将是真实的。不要紧,如果几个线检查一次。他们都将得到相同的答案,而且,不会有冲突。
第二次检查,内部的锁是必要的,因为另外一个线程可能已经抓住了锁第一和完成的初始化过程。
什么你问的是,是否访问的一个领域中的一方法的多次原子--这个问题的答案是否定的。
在上面的例子中,初始化程序是错误的,因为它可能导致多个初始化。你会需要检查 s_Initialized
标志里面锁以及以外,为防止竞争条件在其中多个螺纹读取 s_Initialized
标志之前的任何他们实际上并的初始代码。E.g.,
private static void Initialize()
{
if (s_Initialized)
return;
lock(s_lock)
{
if (s_Initialized)
return;
s_Provider = new MembershipProvider ( ... )
s_Initialized = true;
}
}
Ack,算了...如指出的那样,这确实是不正确的。它不会阻止第二线进入"初始"代码部分。Bah.
你也可以装饰s_Initialized与挥发性的关键词和放弃使用的锁。