当前位置:  开发笔记 > 编程语言 > 正文

在C#中访问变量是一个原子操作吗?

如何解决《在C#中访问变量是一个原子操作吗?》经验,为你挑选了4个好方法。

我已经被认为如果多个线程可以访问变量,那么所有对该变量的读取和写入都必须受到同步代码的保护,例如"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字段在锁外读取?另一个线程难道不能同时写入它吗?变量的读写是否是原子的?



1> John Richard..:

要获得明确的答案,请转到规范.:)

CLI规范第12.6.6节的分区I指出:"符合要求的CLI应保证当对位置的所有写入访问的大小相同时,对不大于本机字大小的正确对齐的内存位置的读写访问权限是原子的. ".

这样就确认了s_Initialized永远不会不稳定,并且对小于32位的原始类型的读写是原子的.

特别是,double并且long(Int64UInt64)保证在32位平台上是原子的.您可以使用Interlocked类上的方法来保护它们.

此外,虽然读取和写入是原子的,但存在具有加法,减法以及递增和递减基元类型的竞争条件,因为它们必须被读取,操作和重写.互锁类允许您使用CompareExchangeIncrement方法保护它们.

互锁会创建一个内存屏障,以防止处理器重新排序读取和写入.在此示例中,锁定创建了唯一必需的屏障.


C#4规范`5.5变量引用的原子性以下数据类型的读写是原子的:bool,char,byte,sbyte,short,ushort,uint,int,float和reference类型.此外,在先前列表中具有基础类型的枚举类型的读取和写入也是原子的.其他类型的读写,包括long,ulong,double和decimal,以及用户定义的类型,不保证是原子的.除了为此目的而设计的库函数之外,不保证原子读 - 修改 - 写,例如在递增或递减的情况下.
虽然访问不大于本机字大小的内存位置是原子的,但由于读/写重新排序,问题中提供的示例代码不是线程安全的.有关详细信息,请参阅我的回答

2> Thomas Danec..:

这是双重检查锁定模式的一种(坏)形式,它在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还写了一篇关于这个问题的文章:双重检查锁定的破坏变种



3> Leon Bambric..:

谈谈 - 标题中的问题绝对不是罗里提出的真正问题.

这个名义上的问题有一个简单的答案"不" - 但当你看到真正的问题时,这根本没有任何帮助 - 我认为没有人给出一个简单的答案.

Rory提出的真实问题在很晚才提出,与他给出的例子更为相关.

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

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

s_Initialized字段在锁外读取,因为锁很昂贵.

由于s_Initialized字段基本上是"一次写入",因此它永远不会返回误报.

在锁外读取它是经济的.

这是一种低成本的具有活性高的有效益的机会.

这就是为什么它在锁外读 - 为了避免支付使用锁的成本,除非它被指出.

如果锁很便宜,代码会更简单,省略第一次检查.

(编辑:来自rory的很好的响应如下.是的,布尔读取是非常原子的.如果有人构建了一个非原子布尔读取的处理器,它们将被添加到DailyWTF上.)



4> Rory MacLeod..:

正确的答案似乎是,"是的,主要是."

    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,以确保处理器重新排序读取和写入时不会破坏锁定.


因为Interlocked.Increment在一个原子操作中执行加载,递增和存储.
推荐阅读
贴进你的心聆听你的世界
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有