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;
}
}
令我惊讶的是,即使在执行了整整三分钟之后,这一断言仍然没有失败。
有什么好处
Debug.Assert
更改为if(..)throw
)配置文件中分别在两个不同的环境中运行了此功能:
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]