在C#中访问一个variables是一个primefaces操作?

我已经提出相信,如果多个线程可以访问一个variables,那么所有对该variables的读取和写入都必须由同步代码保护,例如“locking”语句,因为处理器可能在半途切换到另一个线程一个写。

不过,我正在通过使用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字段在锁之外被读取? 难道另一个线程不能同时写入它吗? 是读写variables的primefaces吗?

为确定的答案去规范。 🙂

CLI规范第I篇第12.6.6节的I分区规定:“一个符合的CLI应保证当所有的写操作访问的位置大小相同时,对正确alignment的不大于本地字大小的内存位置的读写访问是primefaces的“。

因此,确认s_Initialized永远不会不稳定,读取和写入小于32位的原始types是primefaces的。

特别是doublelongInt64UInt64不能保证在32位平台上是primefaces的。 您可以使用Interlocked类中的方法来保护这些方法。

此外,虽然读取和写入是primefaces性的,但是由于必须读取,操作和重写,所以存在与加,减,增和减原语types的竞争条件。 联锁类允许您使用CompareExchangeIncrement方法保护这些类。

联锁创build一个内存屏障,以防止处理器重新sorting读取和写入。 锁在这个例子中创build了唯一必需的障碍。

这是双重检查locking模式的(坏)forms,在C#中不是线程安全的!

这个代码有一个大问题:

s_Initialized不是易失性的。 这意味着在初始化代码中的写入可以在s_Initialized设置为true之后移动,并且即使s_Initialized为true,其他线程也可以看到未初始化的代码。 这不适用于Microsoft的框架实现,因为每个写是一个易失性的写。

而且在微软的实现中,未初始化数据的读取可以被重新sorting(即由CPU预取),所以如果s_Initialized为true,那么读取应该被初始化的数据可能导致读取旧的,未初始化的数据,因为caching命中读取被重新sorting)。

例如:

 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是易失性的,在s_Initialized读取之前,s_Provider的读取将不会被允许移动,并且在s_Initialized被设置为true并且一切正常的情况下,Provider的初始化不被允许移动。

Joe Duffy还写了一篇关于这个问题的文章: 关于双重检查locking的破坏变体

喋喋不休 – 标题中的问题绝对不是罗里所要求的真正的问题。

有名无实的问题有一个“不”的简单回答 – 但是当你看到真正的问题 – 我不认为有人给出了一个简单的答案时,这完全没有帮助。

罗里所要求的真正问题在很晚之后才提出,而且与他给出的例子更为相关。

为什么s_Initialized字段在锁之外被读取?

答案也很简单,尽pipe与variables访问的primefaces性完全无关。

s_Initialized字段是在锁之外读取的,因为锁是昂贵的

由于s_Initialized字段本质上是“一次写入”,它永远不会返回一个误报。

在锁外读取是很经济的。

这是一个低成本的活动,很有可能获得好处。

这就是为什么它在锁外读取 – 为了避免支付使用锁的费用,除非它被指出。

如果锁是便宜的代码会更简单,并省略首先检查。

(编辑:来自rory的良好回应如下:叶,布尔读取是非常primefaces的,如果有人用非primefaces布尔读取构build处理器,他们将在DailyWTF上显示。

正确的答案似乎是,“主要是的”。

  1. John引用CLI规范的答案指出,在32位处理器上访问不超过32位的variables是primefaces的。
  2. 来自C#规范的进一步确认,第5.5节, variables引用的primefaces性 :

    以下数据types的读写是primefaces的:bool,char,byte,sbyte,short,ushort,uint,int,float和referencetypes。 另外,在前面列表中读取和写入带有基础types的枚举types也是primefaces的。 其他types(包括long,ulong,double和decimal)以及用户定义types的读取和写入不保证是primefaces性的。

  3. 我的示例中的代码是从Membership类中改写的,由ASP.NET团队自己编写,所以假设访问s_Initialized字段的方式是正确的,总是安全的。 现在我们知道为什么。

编辑:正如Thomas Danecker指出的那样,即使字段的访问是primefaces的,s_Initialized也应该被标记为volatile ,以确保处理器重新sorting读取和写入的locking不被破坏。

初始化function有问题。 它应该看起来更像这样:

 private static void Initialize() { if(s_initialized) return; lock(s_lock) { if(s_Initialized) return; s_Initialized = true; } } 

如果没有第二次检查locking,初始化代码将被执行两次。 所以第一个检查是为了节省你不必要的锁,第二个检查是针对线程执行初始化代码但尚未设置s_Initialized标志的情况,所以第二个线程会通过第一个检查并在locking等待。

读取和写入variables不是primefaces的。 您需要使用同步API来模拟primefaces读取/写入。

有关这方面的一个很棒的参考资料以及许多与并发有关的问题,请确保您抓取Joe Duffy 最新奇观的副本。 这是一个开膛手!

“在C#中访问一个variables是一个primefaces操作吗?

不。 这不是一个C#的东西,也不是一个.net的东西,这是一个处理器的东西。

OJ是乔Duffy是这个types的信息的人。 如果你想了解更多信息,“互锁”是一个很好的search术语。

“撕裂的读取”可能出现在任何值的字段总和超过指针的大小。

@Leon
我明白了你的观点,就像我问过的那样,然后评论一下,这个问题可以用几种不同的方式来解决。

为了清楚起见,我想知道是否可以安全地让并发线程读写一个布尔型的字段,而不需要任何显式的同步代码,也就是说,访问一个布尔(或其他原始types的)variablesprimefaces。

然后我使用了Membership代码来给出一个具体的例子,但是引入了一些干扰,比如双重检查locking,事实上s_Initialized只能设置一次,而且我注释了初始化代码本身。

我的错。

你也可以用volatile关键字来装饰s_Initialized,并且完全放弃使用locking。

这是不正确的。 您仍然会遇到第二个线程在第一个线程有机会设置标志之前通过检查的问题,这将导致多次执行初始化代码。

我想你是问,如果s_Initialized在锁外读取时可能处于不稳定的状态。 最简洁的答案是不。 一个简单的赋值/读取将归结为一个单独的汇编指令,这个汇编指令在我能想到的每个处理器上都是primefaces的。

我不知道是什么情况下分配到64位variables,这取决于处理器,我会认为这不是primefaces,但它可能是现代32位处理器,当然在所有64位处理器。 复杂的值types的分配不会是primefaces的。

我以为他们是 – 我不确定你的例子中的locking点,除非你同时为s_Provider做了一些事情 – 那么locking将确保这些调用一起发生。

/ / //Perform initialization评论封面创builds_Provider? 例如

 private static void Initialize() { if (s_Initialized) return; lock(s_lock) { s_Provider = new MembershipProvider ( ... ) s_Initialized = true; } } 

否则,静态属性get只是要返回null。

也许联锁提供了一个线索。 否则, 这一个我很好。

我会猜想他们不是primefaces的。

为了使您的代码始终在弱sorting的体系结构上工作,在写s_Initialized之前,必须先放置一个MemoryBarrier。

 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; 

内存写入发生在MembershipProvider构造函数中,并且写入s_Provider不能保证发生,然后在弱有序的处理器上写入s_Initialized。

在这个线程中的很多想法是关于是否是primefaces的东西。 这不是问题。 问题是您的线程写入的顺序对其他线程可见 。 在微弱有序的体系结构中,写入内存不是按顺序发生,这是真正的问题,而不是variables是否适合数据总线。

编辑:其实,我在我的声明混合平台。 在C#中,CLR规范要求写操作是按顺序全局可见的(如果需要,对每个存储使用昂贵的存储指令)。 因此,你不需要在那里真正拥有那个记忆障碍。 但是,如果是C或C ++,而不存在全局可见性顺序的保证,并且目标平台可能具有弱有序内存,并且它是multithreading的,那么在更新s_Initialized之前,需要确保构造函数写入是全局可见的,这是在锁外进行testing。

If (itisso) {检查一个布尔值是primefaces的,但即使没有,也不需要locking第一个检查。

如果任何线程已经完成初始化,那么它将是真实的。 多个线程一次检查并不重要。 他们都会得到同样的答案,而且不会有冲突。

锁内的第二个检查是必要的,因为另一个线程可能已经先取得了锁并已经完成了初始化过程。

你所要问的是在一个方法中多次访问一个字段的primefaces – 答案是否定的。

在上面的例子中,初始化例程是错误的,因为它可能导致多个初始化。 您需要检查locking内部以及外部的s_Initialized标志,以防止在多个线程实际执行初始化代码之前multithreading读取s_Initialized标志的竞争状态。 例如,

 private static void Initialize() { if (s_Initialized) return; lock(s_lock) { if (s_Initialized) return; s_Provider = new MembershipProvider ( ... ) s_Initialized = true; } } 

确认,不要指出,这确实是不正确的。 它不会阻止第二个线程进入“初始化”代码部分。 呸。

你也可以用volatile关键字来装饰s_Initialized,并且完全放弃使用locking。