I asked this question and got this interesting (and a little disconcerting) answer.
Daniel states in his answer (unless I'm reading it incorrectly) that the ECMA-335 CLI specification could allow a compiler to generate code that throws a NullReferenceException
from the following DoCallback
method.
class MyClass {
private Action _Callback;
public Action Callback {
get { return _Callback; }
set { _Callback = value; }
}
public void DoCallback() {
Action local;
local = Callback;
if (local == null)
local = new Action(() => { });
local();
}
}
He says that, in order to guarantee a NullReferenceException
is not thrown, the volatile
keyword should be used on _Callback
or a lock
should be used around the line local = Callback;
.
Can anyone corroborate that? And, if it's true, is there a difference in behavior between Mono and .NET compilers regarding this issue?
Edit
Here is a link to the standard.
Update
I think this is the pertinent part of the spec (12.6.4):
Conforming implementations of the CLI are free to execute programs
using any technology that guarantees, within a single thread of
execution, that side-effects and exceptions generated by a thread are
visible in the order specified by the CIL. For this purpose only
volatile operations (including volatile reads) constitute visible
side-effects. (Note that while only volatile operations constitute
visible side-effects, volatile operations also affect the visibility
of non-volatile references.) Volatile operations are specified in
§12.6.7. There are no ordering guarantees relative to exceptions
injected into a thread by another thread (such exceptions are
sometimes called "asynchronous exceptions" (e.g.,
System.Threading.ThreadAbortException).
[Rationale: An optimizing
compiler is free to reorder side-effects and synchronous exceptions to
the extent that this reordering does not change any observable program
behavior. end rationale]
[Note: An implementation of the CLI is
permitted to use an optimizing compiler, for example, to convert CIL
to native machine code provided the compiler maintains (within each
single thread of execution) the same order of side-effects and
synchronous exceptions.
So... I'm curious as to whether or not this statement allows a compiler to optimize the Callback
property (which accesses a simple field) and the local
variable to produce the following, which has the same behavior in a single thread of execution:
if (_Callback != null) _Callback();
else new Action(() => { })();
The 12.6.7 section on the volatile
keyword seems to offer a solution for programmers wishing to avoid the optimization:
A volatile read has "acquire semantics" meaning that the read is
guaranteed to occur prior to any references to memory that occur after
the read instruction in the CIL instruction sequence. A volatile write
has "release semantics" meaning that the write is guaranteed to happen
after any memory references prior to the write instruction in the CIL
instruction sequence. A conforming implementation of the CLI shall
guarantee this semantics of volatile operations. This ensures that all
threads will observe volatile writes performed by any other thread in
the order they were performed. But a conforming implementation is not
required to provide a single total ordering of volatile writes as seen
from all threads of execution. An optimizing compiler that converts
CIL to native code shall not remove any volatile operation, nor shall
it coalesce multiple volatile operations into a single operation.
See Question&Answers more detail:
os