17

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

三部分问题:

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

6 回答 6

18

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

于 2009-02-27T18:48:21.893 回答
12

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

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

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

于 2009-02-27T19:40:24.420 回答
7

MSDN

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

但是也:

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

于 2009-02-27T18:51:01.387 回答
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 问题?

于 2009-02-27T19:35:27.067 回答
1

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

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

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

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

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

于 2009-02-27T18:53:31.537 回答
0

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

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

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

于 2009-02-27T18:53:58.853 回答