为什么这段代码不能证明读/写的非原子性?

Ani*_*Ani 11 .net c# double atomic thread-safety

阅读 这个问题,我想测试一下我是否可以证明对这种操作的原子性无法保证的类型的读写非原子性.

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

令我惊讶的是,即使在执行了整整三分钟之后,断言也拒绝失败.是什么赋予了?

  1. 测试不正确.
  2. 测试的具体时序特性使得断言不太可能/不可能失败.
  3. 概率是如此之低,以至于我必须运行测试更长时间以使其可能会触发.
  4. 与C#规范相比,CLR提供了更强的原子性保证.
  5. 我的OS /硬件提供比CLR更强的保证.
  6. 别的什么?

当然,我不打算依赖规范没有明确保证的任何行为,但我想更深入地了解这个问题.

仅供参考,我在两个独立环境中的调试和发布(更改Debug.Assertif(..) throw)配置文件上运行此操作:

  1. Windows 7 64位+ .NET 3.5 SP1
  2. Windows XP 32位+ .NET 2.0

编辑:为了排除John Kugelman的评论"调试器不是Schrodinger安全"的可能性,我将该行添加someList.Add(dCopy);到该KeepReading方法并验证该列表没有从缓存中看到单个过时值.

编辑:根据丹·布莱恩特的建议:使用long而不是double几乎立即休息.

Dan*_*ant 12

您可以尝试通过CHESS运行它以查看它是否可以强制交错打破测试.

如果您查看x86 diassembly(从调试器中可见),您可能还会看到抖动是否正在生成保持原子性的指令.


编辑:我继续运行反汇编(强制目标x86).相关的路线是:

                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] 
Run Code Online (Sandbox Code Playgroud)

它使用单个fstp qword ptr在两种情况下执行写操作.我的猜测是英特尔CPU保证了此操作的原子性,但我还没有找到任何文档来支持这一点.任何x86大师谁可以证实这一点?


更新:

如果使用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 
Run Code Online (Sandbox Code Playgroud)

更新:

我很好奇如果我在内存中强制非双字段对齐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;
        }
    }
Run Code Online (Sandbox Code Playgroud)

它不会失败,生成的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] 
Run Code Online (Sandbox Code Playgroud)

我尝试交换_d1和_d2用于dCopy/set,并尝试使用FieldOffset 2.所有生成相同的基本指令(上面有不同的偏移量)并且几秒钟后都没有失败(可能数十亿次尝试).鉴于这些结果,我谨慎地相信至少英特尔x86 CPU提供双重加载/存储操作的原子性,无论是否对齐.