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

标签 c# .net thread-safety double atomic

阅读 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. 概率如此之低,以至于我必须运行更长的时间才能使其有可能触发。
  4. CLR 提供了比 C# 规范更强大的原子性保证。
  5. 我的操作系统/硬件提供比 CLR 更强的保证。
  6. 还有别的吗?

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

仅供引用,我在两个独立环境中的调试和发布(将 Debug.Assert 更改为 if(..) throw)配置文件上运行了这个:

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

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

编辑: 根据 Dan Bryant 的建议:使用 long 而不是 double 几乎可以立即打破它。

最佳答案

您可以尝试通过 CHESS 运行它看看它是否可以强制交错来破坏测试。

如果您查看 x86 反汇编(从调试器可见),您可能还会看到抖动是否正在生成保留原子性的指令。


编辑:我继续运行反汇编(强制目标 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] 

在这两种情况下,它都使用单个 fstp qword ptr 来执行写操作。我的猜测是 Intel 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 

更新:

我很好奇如果我在内存中强制对双字段进行非 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] 

我尝试交换 _d1 和 _d2 以便与 d​​Copy/set 一起使用,还尝试将 FieldOffset 设置为 2。所有指令都生成相同的基本指令(上面有不同的偏移量)并且在几秒钟后都没有失败(可能是数十亿次尝试) .考虑到这些结果,我谨慎地相信至少 Intel x86 CPU 提供了双加载/存储操作的原子性,无论对齐如何。

关于c# - 为什么这段代码不能证明读/写的非原子性?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/3679209/

相关文章:

c# - 循环正则表达式匹配

c# - AddOAuth linkedin dotnet core 2.0

c# - 将委托(delegate)定义为函数指针

c# - 从资源中读取文件并将其写入 C# 中的磁盘

java - 非线程安全标准集合的安全发布

c# - 如何在 Windows 时区和 IANA 时区之间进行转换?

c# - DataTable中如何选择数据单个字符的大小写?

.net - x86 类型发布代码不适用于 Windows Server 2008 x64 中的 ACE.OLEDB.4.0 提供程序

c# - TcpClient BeginRead/Send 线程安全吗?

C# 多个控制台应用程序运行和隔离