A shared variable between two threads behaves differently than a shared property

In his excellent C # slicing treatise, Joseph Albahari proposed the following simple program to demonstrate why we need to use some form of memory guard around data that is read and written by multiple streams. A program never ends if you compile it in Release mode and release it without a debugger:

static void Main() { bool complete = false; var t = new Thread(() => { bool toggle = false; while (!complete) toggle = !toggle; }); t.Start(); Thread.Sleep(1000); complete = true; t.Join(); // Blocks indefinitely } 

My question is: why is the next slightly modified version of the above program no longer blocked indefinitely?

 class Foo { public bool Complete { get; set; } } class Program { static void Main() { var foo = new Foo(); var t = new Thread(() => { bool toggle = false; while (!foo.Complete) toggle = !toggle; }); t.Start(); Thread.Sleep(1000); foo.Complete = true; t.Join(); // No longer blocks indefinitely!!! } } 

While the following are still blocked endlessly:

 class Foo { public bool Complete;// { get; set; } } class Program { static void Main() { var foo = new Foo(); var t = new Thread(() => { bool toggle = false; while (!foo.Complete) toggle = !toggle; }); t.Start(); Thread.Sleep(1000); foo.Complete = true; t.Join(); // Still blocks indefinitely!!! } } 

Like the following:

 class Program { static bool Complete { get; set; } static void Main() { var t = new Thread(() => { bool toggle = false; while (!Complete) toggle = !toggle; }); t.Start(); Thread.Sleep(1000); Complete = true; t.Join(); // Still blocks indefinitely!!! } } 
+6
source share
4 answers

In the first example, Complete is a member variable and can be cached in a register for each stream. Since you are not using a lock, updates to this variable cannot be flushed to main memory, and another thread will see an outdated value for this variable.

In the second example, where Complete is a property, you actually call the function on the Foo object to return the value. I assume that although simple variables can be cached in registers, the compiler may not always optimize the actual properties this way.

EDIT:

As for the optimization of automatic properties - I do not think that in this respect there is something that is guaranteed by the specification. You essentially decide whether the compiler / runtime can optimize the receiver / setter or not.

In the case when it is on the same object, it looks like it does. In another case, it seems not. In any case, I would not argue about this. The easiest way to solve this problem is to use a simple member variable and the sign is volotile to ensure that it is always in sync with main memory.

+7
source

This is because in the first fragment you provided, you made a lambda expression that was closed above the complete logical value, so when the compiler overwrites this, it captures a copy of the value, not the link. Similarly, in the second, it captures the link instead of the copy due to the closure of the Foo object, and thus, when the base value changes, the change is noticed due to the link.

+5
source

Other answers explain what happens in technically correct terms. Let me see if I can explain this in English.

The first example says "Loop until this variable location is true." The new thread creates a copy of this location of the variables (because it is a value type) and goes into the loop forever. If this variable were a reference type, it would make a copy of the link, but since the link was in the same memory location, it would work.

The second example says "Loop until this method (getter) returns." The new thread cannot create a copy of the method, so it creates a copy of the link to an instance of the class in question and calls the getter in that instance again until it returns true (re-reading the same variable location that is set to true in the main thread )

The third example is the same as the first. The fact that the private variable is a member of another instance of the class does not matter.

+3
source

Expand on Eric Petroleum's answer .

If we rewrite the program as follows (the behavior is identical, but avoiding the lambda function makes it easier to read disassembly), we can decompose it and see what it really means to โ€œcache the field value in Registerโ€

 class Foo { public bool Complete; // { get; set; } } class Program { static Foo foo = new Foo(); static void ThreadProc() { bool toggle = false; while (!foo.Complete) toggle = !toggle; Console.WriteLine("Thread done"); } static void Main() { var t = new Thread(ThreadProc); t.Start(); Thread.Sleep(1000); foo.Complete = true; t.Join(); } } 

We get the following behavior:

  Foo.Complete is a Field | Foo.Complete is a Property x86-RELEASE | loops forever | completes x64-RELEASE | completes | completes 

in x86-release, the CLR JIT compiles while (! foo.Complete) into this code:

Completed field:

 004f0153 a1f01f2f03 mov eax,dword ptr ds:[032F1FF0h] # Put a pointer to the Foo object in EAX 004f0158 0fb64004 movzx eax,byte ptr [eax+4] # Put the value pointed to by [EAX+4] into EAX (this basically puts the value of .Complete into EAX) 004f015c 85c0 test eax,eax # Is EAX zero? (is .Complete false?) 004f015e 7504 jne 004f0164 # If it is not, exit the loop # start of loop 004f0160 85c0 test eax,eax # Is EAX zero? (is .Complete false?) 004f0162 74fc je 004f0160 # If it is, goto start of loop 

The last two lines are the problem. If eax is zero, then it will just sit there in an infinite loop saying โ€œEAX zero?โ€ Without any code ever changing the value of eax!

Completed property:

 00220155 a1f01f3a03 mov eax,dword ptr ds:[033A1FF0h] # Put a pointer to the Foo object in EAX 0022015a 80780400 cmp byte ptr [eax+4],0 # Compare the value at [EAX+4] with zero (is .Complete false?) 0022015e 74f5 je 00220155 # If it is, goto 2 lines up 

It really looks like a nicer code. While the JIT has built in the getter property (otherwise you will see some call statements going to other functions), in some simple code, to read the Complete field directly, because it is not allowed to cache the variable when it generates a loop, it repeatedly reads memory over and over, and not just reads caselessly

in x64-release, the 64-bit CLR JIT compiles while (! foo.Complete) into this code

Completed field:

 00140245 48b8d82f961200000000 mov rax,12962FD8h # put 12E12FD8h into RAX. 12E12FD8h is a pointer-to-a-pointer in some .NET static object table 0014024f 488b00 mov rax,qword ptr [rax] # Follow the above pointer; puts a pointer to the Foo object in RAX 00140252 0fb64808 movzx ecx,byte ptr [rax+8] # Add 8 to the pointer to Foo object (it now points to the .Complete field) and put that value in ECX 00140256 85c9 test ecx,ecx # Is ECX zero ? (is the .Complete field false?) 00140258 751b jne 00140275 # If nonzero/true, exit the loop 0014025a 660f1f440000 nop word ptr [rax+rax] # Do nothing! # start of loop 00140260 48b8d82f961200000000 mov rax,12962FD8h # put 12E12FD8h into RAX. 12E12FD8h is a pointer-to-a-pointer in some .NET static object table 0014026a 488b00 mov rax,qword ptr [rax] # Follow the above pointer; puts a pointer to the Foo object in RAX 0014026d 0fb64808 movzx ecx,byte ptr [rax+8] # Add 8 to the pointer to Foo object (it now points to the .Complete field) and put that value in ECX 00140271 85c9 test ecx,ecx # Is ECX Zero ? (is the .Complete field true?) 00140273 74eb je 00140260 # If zero/false, go to start of loop 

Property completed

 00140250 48b8d82fe11200000000 mov rax,12E12FD8h # put 12E12FD8h into RAX. 12E12FD8h is a pointer-to-a-pointer in some .NET static object table 0014025a 488b00 mov rax,qword ptr [rax] # Follow the above pointer; puts a pointer to the Foo object in RAX 0014025d 0fb64008 movzx eax,byte ptr [rax+8] # Add 8 to the pointer to Foo object (it now points to the .Complete field) and put that value in EAX 00140261 85c0 test eax,eax # Is EAX 0 ? (is the .Complete field false?) 00140263 74eb je 00140250 # If zero/false, go to the start 

The 64-bit JIT does the same for both properties and fields, unless the field "expands" the first iteration of the loop - this essentially puts it if(foo.Complete) { jump past the loop code } .

In both cases, it does a similar thing for the x86 JIT when working with the property:
- It inserts a method into a direct memory read - It does not cache it and rereads the value each time

I am not sure that in a 64-bit CLR it is not allowed to cache the value of a field in a register, as 32-bit does, but if it is, it does not interfere. Perhaps it will be in the future?

In any case, this illustrates how the behavior is platform dependent and can be changed. Hope this helps :-)

0
source

Source: https://habr.com/ru/post/914639/


All Articles