阅读 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;
}
}
令我惊讶的是,即使在执行了整整三分钟之后,该断言仍拒绝失败。 是什么赋予了?
- 测试不正确。
- 测试的特定时间特性使得断言不太可能/不可能失败。
- 概率如此之低,以至于我必须运行更长的时间才能使其有可能触发。
- CLR 提供了比 C# 规范更强大的原子性保证。
- 我的操作系统/硬件提供比 CLR 更强的保证。
- 还有别的吗?
当然,我不打算依赖规范未明确保证的任何行为,但我想更深入地了解这个问题。
仅供引用,我在两个独立环境中的调试和发布(将 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
几乎可以立即打破它。
最佳答案
您可以尝试通过 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 以便与 dCopy/set 一起使用,还尝试将 FieldOffset 设置为 2。所有指令都生成相同的基本指令(上面有不同的偏移量)并且在几秒钟后都没有失败(可能是数十亿次尝试) .考虑到这些结果,我谨慎地相信至少 Intel x86 CPU 提供了双加载/存储操作的原子性,无论对齐如何。
关于c# - 为什么这段代码不能证明读/写的非原子性?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/3679209/