模拟在C#中撕掉一个双

时间:2012-01-25 18:52:13

标签: c# .net multithreading atomicity double-precision

我在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);
            }
        }

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

    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);
        }
    }

4 个答案:

答案 0 :(得分:12)

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

编辑:几年前学到了很多困难。

答案 1 :(得分:11)

static double s_x;

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

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

最好的方法是通过使用指针故意错误对齐double。将 unsafe 放在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;

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

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

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

答案 2 :(得分:0)

进行一些挖掘,我发现了一些有关x86体系结构上浮点运算的有趣读物:

根据Wikipedia,x86浮点单元在80位寄存器中存储了浮点值:

  

随后的x86处理器集成了这个x87功能   在芯片上使x87指令成为事实上不可或缺的一部分   x86指令集。每个x87寄存器,称为ST(0)通过   ST(7)是80位宽并且在IEEE浮点中存储数字   标准双扩展精度格式。

此另一个SO问题也是相关的:Some floating point precision and numeric limits question

这可以解释为什么,虽然双打是64位,但它们是原子操作的。

答案 3 :(得分:0)

有关它的价值,可以在此处找到此主题和代码示例。

http://msdn.microsoft.com/en-us/magazine/cc817398.aspx