代码之家  ›  专栏  ›  技术社区  ›  Ani

为什么这段代码没有演示读/写的非原子性?

  •  11
  • Ani  · 技术社区  · 14 年前

    this question ,我想测试是否可以证明在一个类型上读写操作的非原子性,而对于这种类型,读写操作的原子性不能得到保证。

    private static double _d;
    
    [STAThread]
    static void Main()
    {
        new Thread(KeepMutating).Start();
        KeepReading();
    }
    
    private static void KeepReading()
    {
        while (true)
        {
            double dCopy = _d;
    
            // In release: if (...) throw ...
            Debug.Assert(dCopy == 0D || dCopy == double.MaxValue); // Never fails
        }
    }
    
    private static void KeepMutating()
    {
        Random rand = new Random();
        while (true)
        {
            _d = rand.Next(2) == 0 ? 0D : double.MaxValue;
        }
    }
    

    令我惊讶的是,即使在执行了整整三分钟之后,这一主张也没有失败。 有什么好处?

    1. 测试不正确。
    2. 测试的特定时间特性使得断言不太可能失败。
    3. CLR提供了比C规范更强大的原子性保证。
    4. 我的操作系统/硬件提供了比CLR更强大的保证。

    当然,我不打算依赖任何规范没有明确保证的行为,但我希望对这个问题有更深入的理解。

    Debug.Assert if(..) throw )两种不同环境中的配置文件:

    1. Windows XP 32位+.NET 2.0

    编辑:为了排除johnkugelman的评论“调试器不是薛定谔安全的”成为问题的可能性,我添加了一行 someList.Add(dCopy); KeepReading 方法并验证此列表未从缓存中看到单个过时值。

    编辑: long 而不是 double 几乎可以瞬间打破它。

    4 回复  |  直到 7 年前
        1
  •  12
  •   Dan Bryant    14 年前

    你可以试试看 CHESS 看看它是否能强制一个中断测试的交错。

    如果您查看x86分解程序集(在调试器中可见),还可能会看到jitter是否正在生成保留原子性的指令。


                    double dCopy = _d;
    00000039  fld         qword ptr ds:[00511650h] 
    0000003f  fstp        qword ptr [ebp-40h]
    
                    _d = rand.Next(2) == 0 ? 0D : double.MaxValue;
    00000054  mov         ecx,dword ptr [ebp-3Ch] 
    00000057  mov         edx,2 
    0000005c  mov         eax,dword ptr [ecx] 
    0000005e  mov         eax,dword ptr [eax+28h] 
    00000061  call        dword ptr [eax+1Ch] 
    00000064  mov         dword ptr [ebp-48h],eax 
    00000067  cmp         dword ptr [ebp-48h],0 
    0000006b  je          00000079 
    0000006d  nop 
    0000006e  fld         qword ptr ds:[002423D8h] 
    00000074  fstp        qword ptr [ebp-50h] 
    00000077  jmp         0000007E 
    00000079  fldz 
    0000007b  fstp        qword ptr [ebp-50h] 
    0000007e  fld         qword ptr [ebp-50h] 
    00000081  fstp        qword ptr ds:[00159E78h] 
    

    在这两种情况下,它都使用一个fstp qword ptr来执行写操作。我的猜测是,英特尔CPU保证了这个操作的原子性,尽管我还没有找到任何文档来支持这一点。有谁能证实这一点?


    如果使用Int64,则会像预期的那样失败,Int64使用x86 CPU上的32位寄存器,而不是特殊的FPU寄存器。您可以在下面看到:

                    Int64 dCopy = _d;
    00000042  mov         eax,dword ptr ds:[001A9E78h] 
    00000047  mov         edx,dword ptr ds:[001A9E7Ch] 
    0000004d  mov         dword ptr [ebp-40h],eax 
    00000050  mov         dword ptr [ebp-3Ch],edx 
    

    更新:

    我很好奇,如果我强制内存中双字段的非8字节对齐,这是否会失败,所以我整理了以下代码:

        [StructLayout(LayoutKind.Explicit)]
        private struct Test
        {
            [FieldOffset(0)]
            public double _d1;
    
            [FieldOffset(4)]
            public double _d2;
        }
    
        private static Test _test;
    
        [STAThread]
        static void Main()
        {
            new Thread(KeepMutating).Start();
            KeepReading();
        }
    
        private static void KeepReading()
        {
            while (true)
            {
                double dummy = _test._d1;
                double dCopy = _test._d2;
    
                // In release: if (...) throw ...
                Debug.Assert(dCopy == 0D || dCopy == double.MaxValue); // Never fails
            }
        }
    
        private static void KeepMutating()
        {
            Random rand = new Random();
            while (true)
            {
                _test._d2 = rand.Next(2) == 0 ? 0D : double.MaxValue;
            }
        }
    

    它不会失败,生成的x86指令与以前基本相同:

                    double dummy = _test._d1;
    0000003e  mov         eax,dword ptr ds:[03A75B20h] 
    00000043  fld         qword ptr [eax+4] 
    00000046  fstp        qword ptr [ebp-40h] 
                    double dCopy = _test._d2;
    00000049  mov         eax,dword ptr ds:[03A75B20h] 
    0000004e  fld         qword ptr [eax+8] 
    00000051  fstp        qword ptr [ebp-48h] 
    

        2
  •  4
  •   John Kugelman    14 年前

    编译器被允许优化掉文件的重复读取 _d _d级

    要防止这种情况,您需要同步访问 _d级 (即用一个 lock 语句)或标记 _d级 作为 volatile . 使其易失性告诉编译器,它的值随时可能更改,因此它不应该缓存该值。

    double 字段组件 不稳定的 双重的 无法以原子方式访问!同步访问 _d级

        3
  •  2
  •   Peter Johansson    14 年前

    这样两个线程就可以同时读/写同一个变量。

    http://msdn.microsoft.com/en-us/library/system.double.aspx

    但是,如果两个线程都在读/写同一个变量实例,则:

    http://msdn.microsoft.com/en-us/library/system.double.aspx

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

    因此,如果两个线程都在读/写同一个变量实例,则需要一个锁来保护它(或者联锁读/增量/交换(不确定这是否适用于双打)

    编辑

        Public Const ThreadCount As Integer = 2
        Public thrdsWrite() As Threading.Thread = New Threading.Thread(ThreadCount - 1) {}
        Public thrdsRead() As Threading.Thread = New Threading.Thread(ThreadCount - 1) {}
        Public d As Int64
    
        <STAThread()> _
        Sub Main()
    
            For i As Integer = 0 To thrdsWrite.Length - 1
    
                thrdsWrite(i) = New Threading.Thread(AddressOf Write)
                thrdsWrite(i).SetApartmentState(Threading.ApartmentState.STA)
                thrdsWrite(i).IsBackground = True
                thrdsWrite(i).Start()
    
                thrdsRead(i) = New Threading.Thread(AddressOf Read)
                thrdsRead(i).SetApartmentState(Threading.ApartmentState.STA)
                thrdsRead(i).IsBackground = True
                thrdsRead(i).Start()
    
            Next
    
            Console.ReadKey()
    
        End Sub
    
        Public Sub Write()
    
            Dim rnd As New Random(DateTime.Now.Millisecond)
            While True
                d = If(rnd.Next(2) = 0, 0, Int64.MaxValue)
            End While
    
        End Sub
    
        Public Sub Read()
    
            While True
                Dim dc As Int64 = d
                If (dc <> 0) And (dc <> Int64.MaxValue) Then
                    Console.WriteLine(dc)
                End If
            End While
    
        End Sub
    
        4
  •  0
  •   Soonts    14 年前

    我认为正确答案是5。

    double是8字节长。

    还有CPU缓存。在我的机器上,缓存线是64字节,在所有CPU上是8的倍数。

    如上所述,即使CPU在32位模式下运行,也只需一条指令就可以加载和存储双变量。