I've often heard that in the .NET 2.0 memory model, writes always use
release fences. Is this true?
It depends on what model you are referring to.
First, let us precisely define a release-fence barrier. Release semantics stipulate that no other read or write appearing before the barrier in the instruction sequence is allowed to move after that barrier.
- The ECMA specification has a relaxed model in which writes do not provide this guarantee.
- It has been cited somewhere that the CLR implementation provided by Microsoft strengthens the model by making writes have release-fence semantics.
- The x86 and x64 architectures strengthen the model by making writes release-fence barriers and reads acquire-fence barriers.
So it is possible that another implementation of the CLI (such as Mono) running on an esoteric architecture (like ARM which Windows 8 will now target) would not provide release-fence semantics on writes. Notice that I said it is possible, but not certain. But, between all of the memory models in play, such as the different software and hardware layers, you have to code for the weakest model if you want your code to be truly portable. That means coding against the ECMA model and not making any assumptions.
We should make a list of the memory model layers in play just be explicit.
- Compiler: The C# (or VB.NET or whatever) can move instructions.
- Runtime: Obviously the CLI runtime via the JIT compiler can move instructions.
- Hardware: And of course the CPU and memory architecture comes into play as well.
Does this mean that even without explicit memory-barriers or locks, it
is impossible to observe a partially-constructed object (considering
reference-types only) on a thread different from the one on which it
is created?
Yes (qualified): If the environment in which the application is running is obscure enough then it might be possible for a partially constructed instance to be observed from another thread. This is one reason why double-checked locking pattern would be unsafe without using volatile
. In reality, however, I doubt you would ever run into this mostly because Microsoft's implementation of the CLI will not reorder instructions in this manner.
Would it be possible with the following code to observe any output
other than "John 20" and "Jack 21", say "null 20" or "Jack 0" ?
Again, that is qualified yes. But for the some reason as above I doubt you will ever observe such behavior.
Though, I should point out that because person
is not marked as volatile
it could be possible that nothing is printed at all because the reading thread may always see it as null
. In reality, however, I bet that Console.WriteLine
call will cause the C# and JIT compilers to avoid the lifting operation that might otherwise move the read of person
outside the loop. I suspect you are already well aware of this nuance already.
Does this also mean that I can just make all shared fields of
deeply-immutable reference-types volatile and (in most cases) get on
with my work?
I do not know. That is a pretty loaded question. I am not comfortable answering either way without a better understanding of the context behind it. What I can say is that I typically avoid using volatile
in favor of more explicit memory instructions such as the Interlocked
operations, Thread.VolatileRead
, Thread.VolatileWrite
, and Thread.MemoryBarrier
. Then again, I also try to avoid no-lock code altogether in favor of the higher level synchronization mechanisms such as lock
.
Update:
One way I like visualize things is to assume that the C# compiler, JITer, etc. will optimize as aggressively as possible. That means that Person.ctor
might be a candidate for inlining (since it is simple) which would yield the following pseudocode.
Person ref = allocate space for Person
ref.Name = name;
ref.Age = age;
person = instance;
DoSomething(person);
And because writes have no release-fence semantics in the ECMA specification then the other reads & writes could "float" down past the assignment to person
yielding the following valid sequence of instructions.
Person ref = allocate space for Person
person = ref;
person.Name = name;
person.Age = age;
DoSomething(person);
So in this case you can see that person
gets assigned before it is initialized. This is valid because from the perspective of the executing thread the logical sequence remains consistent with the physical sequence. There are no unintended side-effects. But, for reasons that should be obvious, this sequence would be disastrous to another thread.