阅读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;
}
}
令我惊讶的是,即使在执行了整整三分钟之后,断言也拒绝失败。 是什么赋予了?
当然,我并不打算依赖规范未明确保证的任何行为,但我希望对此问题有更深入的了解。
仅供参考,我在两个不同的环境中对Debug和Release(将Debug.Assert
更改为if(..) throw
)配置文件进行了此操作:
编辑:为了排除John Kugelman的评论“调试器不是Schrodinger安全”的可能性,我将someList.Add(dCopy);
行添加到KeepReading
方法并验证此列表不是从缓存中看到一个陈旧的值。
编辑:
基于丹·布莱恩特的建议:使用long
代替double
几乎立即打破它。
答案 0 :(得分:12)
您可以尝试通过CHESS运行它,看看它是否可以强制进行交错打破测试。
如果您查看x86 diassembly(从调试器中可见),您可能还会看到抖动是否正在生成保持原子性的指令。
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在两种情况下执行写操作。我的猜测是英特尔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用于dCopy / set,并尝试使用FieldOffset 2.所有生成相同的基本指令(上面有不同的偏移量)并且几秒钟后都没有失败(可能数十亿次尝试) 。鉴于这些结果,我非常自信至少英特尔x86 CPU提供双重加载/存储操作的原子性,无论是否对齐。
答案 1 :(得分:3)
允许编译器优化_d
的重复读取。据他所知,只是静态地分析你的循环,_d
永远不会改变。这意味着它可以缓存该值,并且永远不会重新读取该字段。
要防止这种情况发生,您需要同步_d
的访问权限(例如用lock
语句将其包围),或将_d
标记为volatile
。使其变为volatile会告诉编译器它的值可能随时发生变化,因此它永远不应该缓存该值。
不幸的是(或幸运的是),您无法将double
字段标记为volatile
,正是因为您尝试测试的一点 - double
s无法原子访问!同步对_d
的访问是编译器重新读取值的强制,但这也会破坏测试。哦,好吧!
答案 2 :(得分:2)
您可以尝试删除'dCopy = _d'并在断言中使用_d。
这样两个线程同时读/写同一个变量。
您当前的版本制作了一个_d副本,它创建了一个新实例,所有这些都在同一个线程中,这是一个线程安全的操作:
http://msdn.microsoft.com/en-us/library/system.double.aspx
此类型的所有成员都是线程安全的。似乎修改实例状态的成员实际上返回使用新值初始化的新实例。与任何其他类型一样,读取和写入包含此类实例的共享变量必须通过锁保护,以保证线程安全。
但是如果两个线程都在读/写同一个变量实例,那么:
http://msdn.microsoft.com/en-us/library/system.double.aspx
在所有硬件平台上分配此类型的实例并非线程安全,因为该实例的二进制表示可能太大而无法在单个原子操作中进行分配。
因此,如果两个线程都在读取/写入同一个变量实例,则需要一个锁来保护它(或者Interlocked.Read/Increment/Exchange。,不确定它是否适用于双精度数据)
修改的
正如其他人所指出的,在Intel CPU上读/写double是一个原子操作。但是,如果程序是为X86编译并使用64位整数数据类型,则操作不是原子操作。如以下程序所示。用双重替换Int64,它似乎工作。
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
答案 3 :(得分:0)
IMO的正确答案是#5。
double是8个字节长。
存储器接口为64位=每个模块每个时钟8个字节(即双通道存储器为16个字节)。
还有CPU缓存。在我的机器上,缓存行是64字节,在所有CPU上,它是8的倍数。
正如上面的评论所说,即使CPU以32位模式运行,也只需加载1个指令即可加载和存储双变量。
这就是为什么只要您的双变量对齐(我怀疑公共语言运行时虚拟机为您做对齐),双读和写是原子的。