Why does this code not demonstrate read / write atomicity?

Reading this question , I wanted to check whether I can demonstrate the atomicity of reading and writing by the type for which the atomicity of such operations is not guaranteed.

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

To my surprise, the statement refused to fail even after three minutes of execution. What gives?

  • The test is incorrect.
  • The specific time characteristics of the test make it unlikely / impossible that the statement fails.
  • The probability is so low that I have to run the test much longer so that it can call it.
  • The CLR provides more reliable atomicity guarantees than the C # specification.
  • My OS / hardware provides more reliable warranties than the CLR.
  • Something else?

Of course, I do not intend to rely on any behavior that is not explicitly guaranteed by the specification, but I would like a deeper understanding of the problem.

FYI, I ran this in both Debug and Release reports (changing Debug.Assert to if(..) throw ) in two separate environments:

  • Windows 7 64-bit + .NET 3.5 SP1
  • Windows XP 32-bit + .NET 2.0

EDIT: to exclude the possibility of John Kugelman’s comment “the debugger is not safe for Schrodinger”, I added the line someList.Add(dCopy); to the KeepReading method and checked that there was not a single obsolete value from the cache in this list.

EDIT: Based on Dan Bryant's suggestion: Using long instead of double interrupts it almost instantly.

+11
double c # thread-safety atomic
Sep 09 '10 at 17:55
source share
4 answers

You can try running it through CHESS to make sure that this can force an interlace that breaks the test.

If you look at the x86 graphics (visible from the debugger), you can also see if jitter generates instructions that preserve atomicity.




EDIT: I went ahead and started disassembling (forcing the x86 target). Matching lines:

  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] 

It uses one fstp qword ptr to perform a write operation in both cases. I assume that the Intel processor guarantees the atomicity of this operation, although I have not found any documentation to support it. Any x86 gurus who can confirm this?




UPDATE:

This is not as expected if you are using Int64, which uses 32-bit registers on an x86 processor, rather than special FPU registers. You can see it below:

  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 



UPDATE:

I was curious if this didn't work, if I forcibly align non-8-byte double-field alignment in memory, so I compiled this code:

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

This will not work, and the generated x86 instructions are essentially the same as before:

  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] 

I experimented with replacing _d1 and _d2 for use with dCopy / set, and also tried FieldOffset from 2. Everyone generated the same basic instructions (with different offsets above), and all this didn't work after a few seconds (probably billions of attempts) , I am cautiously sure, considering these results, at least Intel x86 processors provide atomic double-boot / storage operations regardless of alignment.

+12
Sep 09 '10 at 18:21
source share

The compiler allows optimizing repeated reads of _d . As far as he knows, simply statically analyzing your loop, _d never changes. This means that it can cache the value and never reread the field.

To prevent this, you need to either synchronize access to _d (i.e. surround it with the lock statement), or mark _d as volatile . If it changes the value, it tells the compiler that its value can change at any time and therefore should never cache the value.

Unfortunately (or, fortunately), you cannot mark the double field as volatile , precisely because of the point you are trying to check, it is impossible to get double atomically! Syncing access to _d is that the compiler re-reads the value, but that also breaks the test. Oh good!

+3
Sep 09 '10 at 18:10
source share

You can try to get rid of 'dCopy = _d' and just use _d in your statement.

Thus, two threads simultaneously read / write the same variable.

The current version creates a copy of _d, which creates a new instance, all in one thread, which is a safe thread:

http://msdn.microsoft.com/en-us/library/system.double.aspx

All members of this type are thread safe. Members that appear to change the state of an instance actually return a new instance, initialized with a new value. Like any other type, reading and writing to a shared variable that contains an instance of this type must be protected by a lock to ensure thread safety.

However, if both threads read / write one instance of a variable, then:

http://msdn.microsoft.com/en-us/library/system.double.aspx

Assigning an instance of this type is not thread safe on all hardware platforms because the binary representation of this instance may be too large to be assigned in a single atomic operation.

Thus, if both streams read / write to the same instance of a variable, you will need a lock to protect it (or Interlocked.Read/Increment/Exchange., Not sure if this works in double-local numbers)

Edit

As others have noted, an Intel processor reading / writing a double uses an atomic operation. However, if the program is compiled for X86 and uses a 64-bit integer data type, then the operation will not be atomic. As shown in the next program. Replace Int64 with a double and it seems to work.

  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 
+2
Sep 09 '10 at 18:19
source share

IMO the correct answer is # 5.

double has a length of 8 bytes.

The memory interface is 64 bits = 8 bytes per module per cycle (i.e. it becomes 16 bytes for dual-channel memory).

There are also processor caches. On my machine, the cache line is 64 bytes, and on all CPUs it has 8 characters.

As mentioned above, even when the processor operates in 32-bit mode, double variables are loaded and stored with just 1 instruction.

So, as long as your double variable is aligned (I suspect a shared-language virtual machine is aligning for you), double reads and writes are atomic.

0
Sep 09 '10 at 19:46
source share



All Articles