Unordered boot in a parallel environment

The following is a snippet from a book by Joe Duffy (concurrent programming on Windows), followed by a piece of code to which this paragraph refers. This piece of code is designed to work in a parallel environment (used by many threads), where this LazyInit<T> class is used to create an am object that is initialized only when a value (of type T) really needs to be used in the code.

I would be grateful if someone could talk in detail about the phased scenario, as a result of which, due to the load of the load, the load may cause a problem. That is, how two or more threads using this class, and the purpose of the link and its fields for variables can be a problem if the load order on one of the threads was loaded first by fields and then by the link, and not as we expect it should be ( first download the link, and then the field values ​​obtained through the link)?

I understand that this rarely happens (failure due to non-commissioned downloads). In fact, I see that one thread may not correctly read the field values ​​at first, not knowing what the reference value is (pointer?), But if this happens, this thread will correct itself (as if it were not in a parallel environment) if he noticed that the premature load value is incorrect; in this case, the download will ultimately succeed. In other words, how can the presence of another thread make the bootstrap not “realize” that the load of the boot from the bootstrap is unacceptable?

I hope I managed to convey the problem since I really see it.

Excerpt:

Since all the processors mentioned above, in addition to the .NET memory model, allow reordering of load to load in some cases, loading m_value can move after loading the fields of an object. The effect would be similar and the m_value marking, as volatility prevents it. Marking the fields of objects as variable is not required, since reading the value is a fence and prevents the movement of subsequent loads, regardless of whether they are variable or not. This may seem ridiculous to some: how could a field be read before a reference to the object itself? This seems to violate the data dependency, but it is not: some new processors (like IA64) use cost speculation and will load ahead of time. If the processor guesses the correct value of the link and the field, as it was before the link was written, speculative reading could resign and create a problem. This reordering is quite rare and may never happen in practice, but, nevertheless, this is a problem.

Code example:

 public class LazyInitOnlyOnceRef<T> where T : class { private volatile T m_value; private object m_sync = new object(); private Func<T> m_factory; public LazyInitOnlyOnceRef(Func<T> factory) { m_factory = factory; } public T Value { get { if (m_value == null) { lock (m_sync) { if (m_value == null) m_value = m_factory(); } } return m_value; } } } 
+7
multithreading c # concurrency
source share
3 answers

Some newer processors (such as IA64) use value speculation and will load ahead of time. If the processor is aware of the correct value of the link and the field, as it was before the link was written, speculative reading could resign and create a problem.

This essentially corresponds to the following source transformation:

 var obj = this.m_value; Console.WriteLine(obj.SomeField); 

becomes

 [ThreadStatic] static object lastValueSeen = null; //processor-cache //... int someFieldValuePrefetched = lastValueSeen.SomeField; //prefetch speculatively if (this.m_value == lastValueSeen) { //speculation succeeded (accelerated case). The speculated read is used Console.WriteLine(someFieldValuePrefetched); } else { //speculation failed (slow case). The speculated read is discarded. var obj = this.m_value; lastValueSeen = obj; //remember last value Console.WriteLine(obj.SomeField); } 

The processor is trying to predict the next memory address that will be needed to heat the caches.

Essentially, you can no longer rely on data dependencies because the field can be loaded before the pointer to the containing object is known.


You are asking:

if (this.m_value == lastValueSeen) is really a statement by which prdeiction (based on value, see the last time on m_value) is put into the test. I understand that in sequential programming (not at the same time), the test should always fail if any value was the last, but in parallel programming the test (prediction) could succeed, and the processor thread will begin in an attempt to print an invalid value (i ..e, null someFieldValuePrefetched)

My question is: how could it happen that this false prediction could succeed only in parallel programming, but not in sequential, non-competitive programming. And in connection with this issue, in parallel programming, when this processor makes a false prediction, what is the possible value of m_value (i.e. should it be null, non null)?

Regardless of whether speculation works or not, it does not depend on streaming, but on whether this.m_value the same value as the last execution. If it rarely changes, speculation is often successful.

+4
source share

Firstly, I must say that I really appreciate your help in this matter. To hone my understanding, this is how I see it and please correct me if I am wrong.

If thread T1 was supposed to execute the wrong speculative loading path, the following lines of code will execute:

 Thread T1 line 1: int someFieldValuePrefetched = lastValueSeen.SomeField; //prefetch speculatively Thread T1 line 2: if (this.m_value == lastValueSeen) { //speculation succeeded (accelerated case). The speculated read is used Thread T1 line 3: Console.WriteLine(someFieldValuePrefetched); } else { //speculation failed (slow case). The speculated read is discarded. ….. …. } 

On the other hand, the T2 thread will need to execute the following lines of code.

 Thread T2 line 1: old = m_value; Thread T2 line 2: m_value = new object(); Thread T2 line 3: old.SomeField = 1; 

My first question is: what is vlue this.m_value when "Thread T1 line 1" is running? I suppose it is equal to the old m_ value before "Thread T2 line 2" was executed, right? Otherwise, the speculative branch did NOT choose an accelerated path. This leads me to ask if the T2 thread MUST also execute its lines of code out of order. That is, it performs "Thread T2 line 1", "Thread T2 line 3", "Thread T2 line 2", and not "Thread T2 line 1", "Thread T2 line 2", "Thread T2 line 3"? If so, then I believe the volatile keyword also prevents the T2 thread from executing code in order, right?

I see that the thread T1s "Thread T1 line 2" had to run after threads T2 "Thread T2 line 1" and "Thread T2 line 3" and before "Thread T2 line 2", then SomeField in thread T1 will be 1, although this it made no sense, as you noted, because when SomeField becomes 1, m_value is assigned a new value, which has a value of 0 for SomeField

0
source share

If this is still relevant, consider the following code, this is from CPOW by Joe Duffy:

  MyObject mo = new LazyInit<MyObject>(someFactory).Value; int f = mo.field; if (f == 0) { //Do Something... Console.WriteLine(f); } 

the following text is also from the book "If the time period between the initial reading of mo.field in variable f and the subsequent use of f in Console.WriteLine was long enough, the compiler may decide that it will be more efficient to re-read mo.field twice .... the compiler can solve this, if storing the value creates pressure in the register, it will lead to less efficient use of stack space:

  ... if (mo.field == 0) { ////Do Something... Console.WriteLine(mo.field); } 

So, I think this may be a good example of retirement. By the time mo.field is later used, speculative reading of mo may resign and throw a null ref exception, which is definitely a problem.

0
source share

All Articles