Jon Skeet said:
I believe it *actually* avoids any reordering around *any* method
calls. I don't think 12.6.7 leaves much wiggle-room though - acquiring
the lock counts as a volatile read, and releasing the lock counts as a
volatile write, and the reordering prohibitions therefore apply - and
apply to *all* reads and writes, not just reads and writes of that
variable.
Yes, acquiring the lock is a run-time operation. Just as MemoryBarrier,
volatile read, and volatile write are run-time operations and only ensures
the CPU has flushed any new values to RAM and won't reorder side-effects
between the acquire and release semantics.
I'm talking about compiler optimizations (specifically JIT--okay, I should
have been calling it the IL compiler--because the C# to IL compiler doesn't
really have the concept of registers, as I've made reference to).
Note that 12.6.4 says: "(Note that while only volatile operations
constitute visible side-effects, volatile operations also affect the
visibility of non-volatile references.)" It's that affect non-volatile
visibility which I'm talking about.
Again, run-time operations.
Would it be worth me coming up with a small sample problem which shares
data without using any volatile variables? I claim that (assuming I
write it correctly) there won't be a bug - if you can suggest a failure
mode, we could try to reason about a concrete case rather than talking
in the abstract.
The issue I'm talking about will only occur if the JIT optimizes in a
certain way. Let's take an academic example:
internal class Tester {
private Object locker = new Object();
private int number;
private Random random = new Random();
private void UpdateNumber ( ) {
int count = random.Next();
for (int i = 0; i < count; ++i) {
number++;
Trace.WriteLine(number);
}
}
public void DoSomething() {
lock(locker) {
Trace.WriteLine(number);
}
}
}
*if* the JIT optimized the incrementation of number as follows (example x86,
it's been a while; I may have screwed up the offsets...):
for (int i = 0; i < count; ++i)
00000020 xor ebx,ebx
00000022 test ebp,ebp
00000024 jle 00000033
{
number++;
00000026 add edi,1
00000029 add ebx,1
0000002C cmp ebx,ebp
0000002E jl 00000026
00000030 mov dword ptr [esi+0Ch],edi
00000033 pop ebp
....where it's optimized the calculations on number to use a register (edi)
during the loop and assigned that result to number at the end of the loop.
Within a single thread of execution, very valid (because we haven't told it
otherwise with "volatile"); and within the native world: been done for
decades.
Clearly another thread accessing Tester.number isn't going to see any of
those incremental changes.
Even if you wrap that with a lock statement, create a MemoryBarrier, etc.
those are all still run-time operations, it does not give any information to
the JIT about anything within the lock block (which is what I was referring
to by my original comment about certainly not documented...). By the time
the code is loaded into memory (let alone when Monitor.Enter is called) the
compiler has already done its optimizations.
The only thing that could tell the compiler anything about volatility with
respect to compile-time optimizations is something declarative, like
volatile. Yes, writes to fields declared as volatile also get volatile
reads/writes and acquire/release semantics just like Monitor.Enter and
Monitor.Exit; but that's the run-time aspect of it (for the too-smart
processors).