Warning: file_get_contents(/data/phpspider/zhask/data//catemap/2/.net/23.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
C# 为什么';这段代码演示了读/写的非原子性吗?_C#_.net_Thread Safety_Double_Atomic - Fatal编程技术网

C# 为什么';这段代码演示了读/写的非原子性吗?

C# 为什么';这段代码演示了读/写的非原子性吗?,c#,.net,thread-safety,double,atomic,C#,.net,Thread Safety,Double,Atomic,在阅读中,我想测试我是否能够证明在一个类型上读写的非原子性,而这种类型的读写操作的原子性并没有得到保证 private static double _d; [STAThread] static void Main() { new Thread(KeepMutating).Start(); KeepReading(); } private static void KeepReading() { while (true) { double dCop

在阅读中,我想测试我是否能够证明在一个类型上读写的非原子性,而这种类型的读写操作的原子性并没有得到保证

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;
    }
}
令我惊讶的是,即使在执行了整整三分钟之后,这一断言仍然没有失败。 有什么好处

  • 测试不正确
  • 测试的特定定时特性使得断言不太可能失败
  • 概率太低了,我必须运行测试更长的时间,以使它有可能触发
  • CLR提供了比C规范更强大的原子性保证
  • 我的操作系统/硬件提供了比CLR更强大的保证
  • 还有别的吗
  • 当然,我不打算依赖任何规范没有明确保证的行为,但我希望对这个问题有更深入的理解

    仅供参考,我在Debug和Release(将
    Debug.Assert
    更改为
    if(..)throw
    )配置文件中分别在两个不同的环境中运行了此功能:

  • Windows 7 64位+.NET 3.5 SP1
  • Windows XP 32位+.NET 2.0
  • 编辑:为了排除John Kugelman的评论“调试器不是薛定谔安全的”是问题的可能性,我添加了行
    someList.Add(dCopy)
    调用
    KeepReading
    方法,并验证此列表未从缓存中看到任何过时值

    编辑:
    根据Dan Bryant的建议:使用
    long
    而不是
    double
    实际上会立即中断它。

    编译器可以优化
    \u d
    的重复读取。就静态分析循环而言,
    \u d
    永远不会改变。这意味着它可以缓存该值,并且永远不会重新读取该字段

    要防止出现这种情况,您需要同步对
    \u d
    的访问(即用
    锁定
    语句将其包围),或者将
    \u d
    标记为
    易失性
    。使其易失性会告诉编译器其值可能随时更改,因此它不应缓存该值


    不幸的是(或幸运的是),您无法将
    double
    字段标记为
    volatile
    ,这正是因为您尝试测试的点-
    double
    无法自动访问!同步对
    \u d
    的访问是强制编译器重新读取值的关键,但这也破坏了测试。哦,好吧

    您可以尝试去掉'dCopy=\u d',只需在断言中使用\u d即可

    这样,两个线程同时读取/写入同一变量

    您当前的版本制作了_d的副本,该副本在同一线程中创建了一个新实例,这是一个线程安全操作:

    此类型的所有成员都是线程安全的。看似修改实例状态的成员实际上返回一个用新值初始化的新实例。与任何其他类型一样,对包含此类型实例的共享变量的读写必须受到锁的保护,以确保线程安全

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

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

    因此,如果两个线程都在读/写同一个变量实例,则需要一个锁来保护它(或互锁。Read/Increment/Exchange.,不确定这是否适用于double)

    编辑

    正如其他人所指出的,在Intel CPU上读/写双精度是一种原子操作。但是,如果该程序是为X86编译的,并且使用64位整数数据类型,那么该操作将不是原子操作。如以下程序所示。将Int64替换为double,它似乎可以工作

        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
    
    Public Const ThreadCount为整数=2
    Public thrdsWrite()As Threading.Thread=New Threading.Thread(ThreadCount-1){}
    Public thrdsRead()As Threading.Thread=New Threading.Thread(ThreadCount-1){}
    公共数据类型为Int64
    _
    副标题()
    对于i作为整数=0到thrdsWrite.Length-1
    thrdsWrite(i)=新线程。线程(写地址)
    thrdsWrite(i).SetApartmentState(Threading.ApartmentState.STA)
    thrdsWrite(i).IsBackground=True
    thrdsWrite(i).Start()
    thrdsRead(i)=新线程。线程(读地址)
    thrdsRead(i).SetApartmentState(Threading.ApartmentState.STA)
    thrdsRead(i).IsBackground=True
    thrdsRead(i).Start()
    下一个
    Console.ReadKey()
    端接头
    公共子写入()
    Dim rnd作为新随机数(DateTime.Now.毫秒)
    虽然是真的
    d=If(rnd.Next(2)=0,0,Int64.MaxValue)
    结束时
    端接头
    公共子读取()
    虽然是真的
    将直流电压调整为Int64=d
    如果是(dc 0)和(dc Int64.MaxValue),则
    控制台写入线(dc)
    如果结束
    结束时
    端接头
    
    您可以尝试运行它,看看它是否可以强制执行中断测试的交错

    如果查看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保证了此操作的原子性,尽管我还没有找到任何支持此操作的文档。有谁能证实这一点


    更新:

    如果使用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 
    

    更新:

    我是
        [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;
            }
        }
    
                    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]