在C#下使用Int64会对32位处理器造成危险

时间:2009-02-27 18:44:52

标签: c# .net thread-safety int64

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

三部分问题:

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

6 个答案:

答案 0 :(得分:18)

这与您遇到的每个变量无关。如果某个变量用作共享状态或某些内容(包括但不限于某些 static字段),则应该处理此问题。对于由于在闭包或迭代器转换中被关闭而未被提升的局部变量而言,它完全没有问题,并且一次由单个函数(因此,单个线程)使用。

答案 1 :(得分:12)

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

锁定免费编程非常非常难以正确。您需要知道完全您正在做什么,并将复杂性保持为尽可能小的代码。就个人而言,除了非常着名的模式之外,我甚至很少尝试尝试它,例如使用静态初始化程序初始化集合,然后从集合中读取而不锁定。

使用Interlocked类可以在某些情况下提供帮助,但是取出锁定几乎总是容易得多。无可争议的锁是“非常便宜的”(不可否认,它们随着更多内核而变得昂贵,但所有内容也是如此) - 在你有充分的证据证明它实际上会产生重大影响之前,不要乱用无锁代码。 / p>

答案 2 :(得分:7)

MSDN

  

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

但是:

  

与任何其他类型一样,阅读和   写一个共享变量   包含此类型的实例必须   受到锁保护   线程安全。

答案 3 :(得分:2)

如果你有一个共享变量(比如,作为一个类的静态字段,或者作为一个共享对象的字段),并且该字段或对象将被用于跨线程,那么,是的,你需要确保通过原子操作保护对该变量的访问。 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问题?

答案 4 :(得分:1)

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

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

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

这只是一个可能的竞争条件,在32位平台上进行64位分配。

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

答案 5 :(得分:0)

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

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

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