代码之家  ›  专栏  ›  技术社区  ›  Rory MacLeod

访问C中的变量是原子操作吗?

  •  63
  • Rory MacLeod  · 技术社区  · 18 年前

    我一直认为,如果多个线程可以访问一个变量,那么所有对该变量的读取和写入都必须受到同步代码的保护,例如“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_初始化字段?另一个线程不能同时尝试写入它吗? 变量的读写是原子的吗?

    16 回复  |  直到 10 年前
        1
  •  35
  •   Ben    10 年前

    对于确定的答案,请参阅规范。)

    cli规范第12.6.6节的分区i指出:“当对某个位置的所有写入访问大小相同时,一致的cli应确保对不大于本机字大小的正确对齐内存位置的读写访问是原子的。”

    因此,这证实了S_初始化永远不会是不稳定的,对小于32位的primitve类型的读写是原子的。

    特别地, double 和 long ( Int64 和 UInt64 是 不 保证在32位平台上是原子的。您可以在 Interlocked 类来保护这些。

    此外,虽然读和写是原子的,但有一个加、减、递增和递减基本类型的竞争条件,因为它们必须被读取、操作和重写。联锁类允许您使用 CompareExchange 和 Increment 方法。

    互锁产生一个内存屏障,以防止处理器重新排序读写。在这个例子中,锁创建了唯一需要的屏障。

        2
  •  34
  •   abatishchev Karl Johan    13 年前

    这是一种(坏的)双重检查锁定模式,即 不 C中的螺纹安全!

    此代码中有一个大问题:

    已初始化的S_不易挥发。这意味着初始化代码中的写入可以在s_initialized设置为true之后移动,而其他线程可以看到未初始化的代码,即使s_initialized对它们为true也是如此。这不适用于Microsoft的框架实现,因为每个写操作都是不稳定的。

    但在Microsoft的实现中,对未初始化数据的读取也可以重新排序(即由CPU预取),因此,如果“初始化”为“真”,则读取应初始化的数据可能会导致由于缓存命中而读取旧的未初始化数据(即重新排序读取)。

    例如:

    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_之前移动S_提供程序的读取是完全合法的,因为任何地方都没有易失性读取。

    如果初始化的s_是不稳定的,则在初始化s_的读取之前,不允许移动s_提供程序的读取,并且在s_初始化设置为true且现在一切正常后,不允许移动提供程序的初始化。

    乔·达菲还写了一篇关于这个问题的文章: Broken variants on double-checked locking

        3
  •  11
  •   Leon Bambrick jon Z    18 年前

    等等——标题上的问题绝对不是罗里问的真正的问题。

    有名无实的问题有一个简单的答案“不”,但当你看到真正的问题时,这根本帮不上什么忙——我认为没有人给出过一个简单的答案。

    罗里提出的真正的问题会在很晚的时候提出,并且与他给出的例子更为相关。

    为什么读取s_初始化字段 在锁外面?

    答案也很简单,尽管与变量访问的原子性完全无关。

    在锁外读取s_初始化字段是因为 锁很贵 .

    由于s_初始化字段本质上是“写入一次”,它将永远不会返回假阳性。

    在锁外面读是很经济的。

    这是一个 低成本 活动与A 高的 获得利益的机会。

    这就是为什么它是在锁外读取的——以避免支付使用锁的费用,除非有指示。

    如果锁便宜的话,代码就更简单了,并且省略了第一次检查。

    (编辑:罗里的回答很好。是的,布尔读取是非常原子化的。如果有人构建了一个具有非原子布尔值读取功能的处理器,那么他们将在dailywtf上显示出来。)

        4
  •  7
  •   Rory MacLeod    18 年前

    正确的答案似乎是,“是的,大多数情况下。”

    1. 约翰引用cli规范的答案表明,访问32位处理器上不超过32位的变量是原子的。
    2. C规范第5.5节进一步确认, Atomicity of variable references :

      以下数据类型的读写是原子的:bool、char、byte、sbyte、short、ushort、uint、int、float和reference类型。此外,前一列表中具有基础类型的枚举类型的读写也是原子的。其他类型的读写(包括long、ulong、double和decimal)以及用户定义的类型不能保证是原子的。

    3. 我的示例中的代码是从成员类中改写的,正如ASP.NET团队自己编写的那样,因此始终可以安全地假定它访问s_初始化字段的方式是正确的。现在我们知道为什么了。

    编辑:正如ThomasDanecker指出的那样,即使字段的访问是原子的,也应该真正标记已初始化的 不稳定的 以确保锁定不会因处理器重新排序读写而中断。

        5
  •  2
  •   Sam Chad    12 年前

    初始化功能出现故障。看起来应该更像这样:

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

    如果不在锁内进行第二次检查,可能会执行两次初始化代码。因此,第一个检查是为了节省您不必要地使用锁的性能,第二个检查是针对线程正在执行初始化代码但尚未设置 s_Initialized 标记,这样第二个线程将通过第一个检查并在锁处等待。

        6
  •  1
  •   OJ.    18 年前

    变量的读写不是原子的。您需要使用同步API来模拟原子读/写。

    要获得关于这方面的出色参考以及与并发性相关的更多问题,请确保获取Joe Duffy的副本 latest spectacle . 这是一个开膛手!

        7
  •  1
  •   Leon Bambrick jon Z    18 年前

    “访问C中的变量是原子操作吗?”

    不。它不是一个C事物,也不是一个.NET事物,它是一个处理器事物。

    OJ就是其中的一员,乔·达菲就是要去了解这类信息的人。如果你想知道更多,“连锁”是一个很好的搜索词。

    “撕裂读取”可以发生在字段总和超过指针大小的任何值上。

        8
  •  1
  •   Rory MacLeod    18 年前

    @列昂
    我明白你的观点——我问过的,然后评论过的,这个问题可以用两种不同的方式来表达。

    为了清楚起见,我想知道在没有任何显式同步代码的情况下,让并发线程读写布尔字段是否安全,即访问布尔(或其他基元类型)变量原子。

    然后我使用成员代码给出了一个具体的例子,但是这引入了一些干扰,比如双重检查锁定,S_初始化的事实只有一次设置,并且我注释了初始化代码本身。

    我的错。

        9
  •  1
  •   Sam Chad    12 年前

    你也可以装饰用volatile关键字初始化的s_,完全放弃使用lock。

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

        10
  •  1
  •   Sam Chad    12 年前

    我想你是在问 s_Initialized 在锁外读取时可能处于不稳定状态。简短的答案是否定的。一个简单的分配/读取将归结为一个单一的汇编指令,它在我能想到的每一个处理器上都是原子的。

    我不确定分配给64位变量是什么情况,这取决于处理器,我假设它不是原子的,但它可能在现代32位处理器上,当然在所有64位处理器上。复杂值类型的赋值将不是原子的。

        11
  •  0
  •   Keith    18 年前

    我认为它们是-我不确定您的示例中的锁的位置,除非您同时对S_提供商做了一些事情-那么锁将确保这些调用一起发生。

    这样做吗 //Perform initialization 评论封面创建供应商?例如

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

    否则,静态属性get将返回空值。

        12
  •  0
  •   Peteter    18 年前

    也许 Interlocked 给出线索。否则 this one 我很好。

    我早就猜到它们不是原子的了。

        13
  •  0
  •   doug65536    14 年前

    要使代码始终在弱顺序的体系结构上工作,必须在编写初始化的s_之前放置一个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_之前不保证发生。

    在这个过程中,有很多想法是关于某个东西是否是原子的。这不是问题所在。问题是 线程的写入顺序对其他线程可见 . 在弱顺序体系结构中,写入内存的顺序并不正确,这是真正的问题,而不是变量是否适合数据总线。

    编辑: 实际上,我在陈述中混合了平台。在C中,clr规范要求写入按顺序是全局可见的(必要时对每个存储使用昂贵的存储指令)。因此,你不需要有记忆障碍。但是,如果C或C++没有全局可见性顺序的这种保证,并且您的目标平台可能具有弱有序的内存,并且它是多线程的,那么您需要确保构造函数在更新SY初始化之前是全局可见的,这是在锁之外测试的。

        14
  •  0
  •   James Anderson    13 年前

    安 If (itisso) { 检查布尔值是原子的,但即使它不是原子的 无需锁定第一张支票。

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

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

        15
  •  0
  •   Sam Chad    12 年前

    你要问的是,是否多次访问一个方法中的一个字段,而答案是否定的。

    在上面的示例中,初始化例程有故障,因为它可能导致多次初始化。你需要检查一下 s_Initialized 标记在锁内和锁外,以防止多个线程读取 S12初始化 在它们中的任何一个实际执行初始化代码之前标记。例如。,

    private static void Initialize()
    {
        if (s_Initialized)
            return;
    
        lock(s_lock)
        {
            if (s_Initialized)
                return;
            s_Provider = new MembershipProvider ( ... )
            s_Initialized = true;
        }
    }
    
        16
  •  -1
  •   VonC    18 年前

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

    你也可以装饰用volatile关键字初始化的s_,完全放弃使用lock。