Let's say you have a simple class like this:
class MyClass
{
private readonly int a;
private int b;
public MyClass(int a, int b) { this.a = a; this.b = b; }
public int A { get { return a; } }
public int B { get { return b; } }
}
I could use this class in a multi-threaded manner:
MyClass value = null;
Task.Run(() => {
while (true) { value = new MyClass(1, 1); Thread.Sleep(10); }
});
while (true)
{
MyClass result = value;
if (result != null && (result.A != 1 || result.B != 1)) {
throw new Exception();
}
Thread.Sleep(10);
}
My question is: will I ever see this (or other similar multi-threaded code) throw an exception? I often see reference to the fact that non-volatile writes might not immediately be seen by other threads. Thus, it seems like this could fail because the write to the value field might happen before the writes to a and b. Is this possible, or is there something in the memory model that makes this (quite common) pattern safe? If so, what is it? Does readonly matter for this purpose? Would it matter if a and b were a type that can't be atomically written (e. g. a custom struct)?
Code as written will work starting from CLR2.0 as the CLR2.0 memory model guarantees that All stores have release semantics.
Release semantics: Ensures no load or store that comes before the fence will move after the fence. Instructions after it may still happen before the fence.(Taken from CPOW Page 512).
Which means that constructor initialization cannot be moved after the assignment of the class reference.
Joe duffy mentioned this in his article about the very same subject.
Rule 2: All stores have release semantics, i.e. no load or store may move after one.
Also Vance morrison's article here confirms the same(Section Technique 4: Lazy Initialization).
Like all techniques that remove read locks, the code in Figure 7 relies on strong write ordering. For example, this code would be incorrect in the ECMA memory model unless myValue was made volatile because the writes that initialize the LazyInitClass instance might be delayed until after the write to myValue, allowing the client of GetValue to read the uninitialized state. In the .NET Framework 2.0 model, the code works without volatile declarations.
Writes are guaranteed to happen in order starting from CLR 2.0. It is not specified in ECMA standard, it is just the microsoft implementation of the CLR gives this guarantee. If you run this code in either CLR 1.0 or any other implementation of CLR, your code is likely to break.
Story behind this change is:(From CPOW Page 516)
When the CLR 2.0 was ported to IA64, its initial development had happened on X86 processors, and so it was poorly equipped to deal with arbitrary store reordering (as permitted by IA64) . The same was true of most code written to target .NET by nonMicrosoft developers targeting Windows
The result was that a lot of code in the framework broke when run on IA64, particularly code having to do with the infamous double-checked locking pattern that suddenly didn't work properly. We'll examine this in the context of the pattern later in this chapter. But in summary, if stores can pass other stores, consider this: a thread might initialize a private object's fields and then publish a reference to it in a shared location; because stores can move around, another thread might be able to see the reference to the object, read it, and yet see the fields while they are still i n an uninitialized state. Not only did this impact existing code, it could violate type system properties such as initonly fields.
So the CLR architects made a decision to strengthen 2.0 by emitting all stores on IA64 as release fences. This gave all CLR programs stronger memory model behavior. This ensures that programmers needn' t have to worry about subtle race conditions that would only manifest in practice on an obscure, rarely used and expensive architecture.
Note Joe duffy says that they strengthen 2.0 by emitting all stores on IA64 as release fences which doesn't mean that other processors can reorder it. Other processors itself inherently provides the guarantee that store-store(store followed by store) will not be reordered. So CLR doesn't need to explicitly guarantee this.