【问题标题】:Under C# is Int64 use on a 32 bit processor dangerous在 C# 下,在 32 位处理器上使用 Int64 是危险的
【发布时间】:2010-10-10 09:52:19
【问题描述】:

我在 MS 文档中读到,在 32 位 Intel 计算机上分配 64 位值不是原子操作;也就是说,该操作不是线程安全的。这意味着如果两个人同时为静态Int64字段赋值,则无法预测该字段的最终值。

三部分题:

  • 这是真的吗?
  • 这是我在现实世界中会担心的事情吗?
  • 如果我的应用程序是多线程的,我真的需要用锁定代码包围我的所有 Int64 分配吗?

【问题讨论】:

标签: c# .net thread-safety int64


【解决方案1】:

这不是关于您遇到的每个变量。如果某些变量被用作共享状态或其他东西(包括但不限于 some static 字段),您应该注意这个问题。对于由于在闭包或迭代器转换中被关闭而没有被提升并且一次由单个函数(因此,单个线程)使用的局部变量,这完全没有问题。

【讨论】:

  • 这是正确的,但可能不清楚原因。 Int64 继承自 System.ValueType,这意味着值存储在堆栈中。由于每个线程都有自己的调用堆栈,因此每个线程都有自己的值,即使在调用同一个函数时也是如此。
  • 想象类 X { int n; }。它是引用类型还是值类型?它会存储在堆中还是堆栈中?
  • DK,我不认为这是一个相关的问题,但类是引用类型并且存储在堆中。如果你只在一个线程中持有一个类的引用,你仍然不需要担心锁定问题。
  • 当心,正如 Eric Lippert 在blogs.msdn.com/b/ericlippert/archive/2010/09/30/… 中解释的那样,关于值类型的堆栈或堆分配的注释应该更正确地表述为:“在桌面 CLR 上的 C# 的 Microsoft 实现中,值当值是局部变量或临时变量且不是 lambda 或匿名方法的封闭局部变量且方法体不是迭代器块且抖动选择不注册该值时,类型存储在堆栈中.".
  • codekaizen 的评论:'An Int64 继承自 System.ValueType,表示该值存储在堆栈中。'它可能存储在堆栈中这一事实是一个实现细节,事实上,并非在所有情况下都是如此。
【解决方案2】:

即使写入原子的,你仍然需要在访问变量时取出锁。如果您不这样做,则至少必须创建变量volatile 以确保所有线程在下次读取变量时都能看到新值(这几乎总是您想要的)。这让你可以做原子的、易失的集合——但只要你想做任何更有趣的事情,比如给它加 5,你就会回到锁定状态。

无锁编程非常非常难以做到正确。您需要确切地知道自己在做什么,并将复杂性控制在尽可能小的代码中。就个人而言,我什至很少尝试尝试它,除了非常知名的模式,例如使用静态初始化器来初始化集合,然后在不锁定的情况下从集合中读取。

使用Interlocked 类在某些情况下会有所帮助,但只需取出锁几乎总是容易得多。无争议的锁是“相当便宜的”(诚然,它们会因为更多的内核而变得昂贵,但一切都是如此)——在你有充分的证据证明它实际上会产生重大影响之前,不要乱用无锁代码。

【讨论】:

    【解决方案3】:

    如果您确实有一个共享变量(例如,作为类的静态字段,或作为共享对象的字段),并且该字段或对象将被跨线程使用,那么,是的,您需要确保通过原子操作保护对该变量的访问。 x86 处理器具有确保发生这种情况的内在函数,并且此功能通过 System.Threading.Interlocked 类方法公开。

    例如:

    class Program
    {
        public static Int64 UnsafeSharedData;
        public static Int64 SafeSharedData;
    
        static void Main(string[] args)
        {
            Action<Int32> unsafeAdd = i => { UnsafeSharedData += i; };
            Action<Int32> unsafeSubtract = i => { UnsafeSharedData -= i; };
            Action<Int32> safeAdd = i => Interlocked.Add(ref SafeSharedData, i);
            Action<Int32> safeSubtract = i => Interlocked.Add(ref SafeSharedData, -i);
    
            WaitHandle[] waitHandles = new[] { new ManualResetEvent(false), 
                                               new ManualResetEvent(false),
                                               new ManualResetEvent(false),
                                               new ManualResetEvent(false)};
    
            Action<Action<Int32>, Object> compute = (a, e) =>
                                                {
                                                    for (Int32 i = 1; i <= 1000000; i++)
                                                    {
                                                        a(i);
                                                        Thread.Sleep(0);
                                                    }
    
                                                    ((ManualResetEvent) e).Set();
                                                };
    
            ThreadPool.QueueUserWorkItem(o => compute(unsafeAdd, o), waitHandles[0]);
            ThreadPool.QueueUserWorkItem(o => compute(unsafeSubtract, o), waitHandles[1]);
            ThreadPool.QueueUserWorkItem(o => compute(safeAdd, o), waitHandles[2]);
            ThreadPool.QueueUserWorkItem(o => compute(safeSubtract, o), waitHandles[3]);
    
            WaitHandle.WaitAll(waitHandles);
            Debug.WriteLine("Unsafe: " + UnsafeSharedData);
            Debug.WriteLine("Safe: " + SafeSharedData);
        }
    }
    

    结果:

    不安全:-24050275641 安全:0

    有趣的是,我在 Vista 64 上以 x64 模式运行它。这表明运行时将 64 位字段视为 32 位字段,也就是说,64 位操作是非原子的。有人知道这是 CLR 问题还是 x64 问题?

    【讨论】:

    • 正如 Jon Skeet 和 Ben S 所指出的那样,读取和写入之间可能会发生争用情况,因此您不能推断写入是非原子的。
    • 我不明白......这个论点是两种方式。据我所知,数据仍然是错误的。如果您运行该示例,很明显由于非原子操作导致数据错误。
    • 这个问题既不是 CLR 也不是 x64。它与您的代码有关。你要做的是原子读+加/减+写。而在 x64 中,您可以保证对 int64 进行原子读/写。同样,这与原子读取+添加+写入不同。
    【解决方案4】:

    这是真的吗?是的,事实证明。如果您的寄存器中只有 32 位,并且您需要将 64 位值存储到某个内存位置,则将需要两次加载操作和两次存储操作。如果您的进程被这两个加载/存储之间的另一个进程中断,则另一个进程可能会损坏您的一半数据!奇怪但真实。这在每个构建的处理器上都是一个问题 - 如果您的数据类型比您的寄存器长,您将遇到并发问题。

    这是我在现实世界中会担心的事情吗?是和不是。由于几乎所有现代编程都有自己的地址空间,因此您只需要在进行多线程编程时担心这一点。

    如果我的应用程序是多线程的,我真的需要用锁定代码包围我的所有 Int64 分配吗?可悲的是,如果你想获得技术,是的。在实践中,在较大的代码块周围使用 Mutex 或 Semaphore 通常比将每个单独的 set 语句锁定在全局可访问的变量上更容易。

    【讨论】:

      【解决方案5】:

      在 32 位 x86 平台上,最大的原子大小的内存是 32 位的。

      这意味着,如果某些内容写入或读取 64 位大小的变量,则该读取/写入可能会在执行期间被抢占。

      • 例如,您开始为 64 位变量赋值。
      • 写入前 32 位后,操作系统决定另一个进程将获得 CPU 时间。
      • 下一个进程尝试读取您正在分配的变量。

      这只是 32 位平台上 64 位分配的一种可能的竞争条件。

      但是,即使使用 32 位变量,也可能存在读写竞争条件,因此任何共享变量都应以某种方式同步以解决这些竞争条件。

      【讨论】:

      • “在 32 位 x86 平台上,最大的原子大小的内存是 32 位的。” - 这是错误的。您可以通过 fstp / mmx / sse 原子地写入 8 字节。
      【解决方案6】:

      MSDN

      分配这种类型的实例是 在所有硬件上都不是线程安全的 平台,因为二进制 该实例的表示可能 太大而不能在一个单一的分配 原子操作。

      还有:

      与任何其他类型一样,阅读和 写入共享变量 包含此类型的实例 must 被锁保护以保证 线程安全。

      【讨论】:

      • 没错,关键字是共享变量
      猜你喜欢
      • 1970-01-01
      • 2010-12-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-06-10
      • 2010-09-19
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多