模拟在C#中撕掉一个双

Mic*_*lli 18 .net c# multithreading double-precision atomicity

我正在使用32位计算机运行,并且我能够使用以下快速命中的代码片段来确认长值可以撕裂.

        static void TestTearingLong()
        {
            System.Threading.Thread A = new System.Threading.Thread(ThreadA);
            A.Start();

            System.Threading.Thread B = new System.Threading.Thread(ThreadB);
            B.Start();
        }

        static ulong s_x;

        static void ThreadA()
        {
            int i = 0;
            while (true)
            {
                s_x = (i & 1) == 0 ? 0x0L : 0xaaaabbbbccccddddL;
                i++;
            }
        }

        static void ThreadB()
        {
            while (true)
            {
                ulong x = s_x;
                Debug.Assert(x == 0x0L || x == 0xaaaabbbbccccddddL);
            }
        }
Run Code Online (Sandbox Code Playgroud)

但是当我尝试与双打类似的东西时,我无法得到任何撕裂.有谁知道为什么?据我从规范中可以看出,只有浮点数的赋值才是原子的.分配给双人应该有撕裂的风险.

    static double s_x;

    static void TestTearingDouble()
    {
        System.Threading.Thread A = new System.Threading.Thread(ThreadA);
        A.Start();

        System.Threading.Thread B = new System.Threading.Thread(ThreadB);
        B.Start();
    }

    static void ThreadA()
    {
        long i = 0;

        while (true)
        {
            s_x = ((i & 1) == 0) ? 0.0 : double.MaxValue;
            i++;

            if (i % 10000000 == 0)
            {
                Console.Out.WriteLine("i = " + i);
            }
        }
    }

    static void ThreadB()
    {
        while (true)
        {
            double x = s_x;

            System.Diagnostics.Debug.Assert(x == 0.0 || x == double.MaxValue);
        }
    }
Run Code Online (Sandbox Code Playgroud)

Eug*_*eck 12

听起来很奇怪,这取决于你的CPU.虽然双打不能保证不撕裂,但它们不会在许多当前的处理器上.如果你想在这种情况下撕裂,试试AMD Sempron.

编辑:了解几年前的艰难道路.


Han*_*ant 11

static double s_x;
Run Code Online (Sandbox Code Playgroud)

使用双精度表演效果要困难得多.CPU使用专用指令来加载和存储双重FLD和FSTP.它是与更容易,因为没有单指令装入/存储一个64位在32位模式下的整数.要观察它,你需要使变量的地址不对齐,以便它跨越cpu缓存行边界.

使用的声明永远不会发生这种情况,JIT编译器确保double正确对齐,存储在8的倍数的地址.您可以将它存储在类的字段中,GC分配器仅对齐到4 in 32位模式.但那是一个废话.

最好的方法是通过使用指针故意错误对齐double.将不安全的东西放在Program类的前面,使它看起来像这样:

    static double* s_x;

    static void Main(string[] args) {
        var mem = Marshal.AllocCoTaskMem(100);
        s_x = (double*)((long)(mem) + 28);
        TestTearingDouble();
    }
ThreadA:
            *s_x = ((i & 1) == 0) ? 0.0 : double.MaxValue;
ThreadB:
            double x = *s_x;
Run Code Online (Sandbox Code Playgroud)

这仍然不能保证良好的错位(hehe),因为无法准确控制AllocCoTaskMem()将相对于cpu缓存行的开头对齐分配的位置.它取决于你的cpu核心中的缓存关联性(我的是Core i5).你必须修补偏移量,我通过实验得到了值28.该值应该可被4整除,但不能被8整除,以真正模拟GC堆行为.继续向该值添加8,直到您获得双倍跨越缓存行并触发断言.

为了减少人为,你必须编写一个程序来存储类的字段中的double,并让垃圾收集器在内存中移动它以使它不对齐.有点难以想出一个确保这种情况发生的示例程序.

另请注意您的程序如何演示称为错误共享的问题.注释掉线程B的Start()方法调用,并注意线程A的运行速度.您正在看到cpu的成本使cpu核心之间的缓存线保持一致.由于线程访问相同的变量,因此此处是共享.当线程访问存储在同一缓存行中的不同变量时,会发生真正的错误共享.这就是为什么对齐很重要的原因,你只能在它的一部分位于一个缓存行中并且其中一部分位于另一个缓存行中时观察到双重撕裂.