When is "volatile" used instead of "lock" ?

J

Jon Skeet [C# MVP]

Peter Ritchie said:
...which only applies to reference types. Most of this discussion has been
revolving around value types (by virtue of Interlocked.Increment), for which
"lock" cannot not apply. e.g. you can't switch from using lock on a member
to using Interlocked.Increment on that member, one works with references and
the other with value types (specifically Int32 and Int64). This is what
raised my concern.

It's not a case of using a lock on a particular value - taking the lock
out creates a memory barrier beyond which *no* reads can pass, not just
reads on the locked expression.
...still doesn't document anything about the members/variables within the
locked block (please read my example). That quote applies only to the
reference used as the parameter for the lock.

There can be no lock acquire semantics for value members. Suggesting
"locking appropriately" cannot apply here and can be misconstrued by some
people by creating something like "lock(myLocker){intMember = SomeMethod();}"
which does not do the same thing as making intMember volatile, increases
overhead needlessly, and still leaves a potential bug.

No, it *doesn't* leave a bug - you've misunderstood the effect of lock
having acquire semantics.
Even if the discussion hasn't been about value types, a dangerous statement;
because it could only apply to reference types (i.e. if myObject is wrapped
with lock(myObject) in every thread, yes I don't need to declare it with
volatile--but that's probably not why I'm using lock). In the context of
reference types, volatile only applies to the pointer (reference) not
anything within the object it references. Reference assignment is atomic,
there's no need to use lock to guard that sort of thing. You use lock to
guard a non-atomic invariant, volatile has nothing to do with that--it has to
do with the optimization (ordering, caching) of pointer/value reads and
writes.

Atomicity and volatility are very different things, and shouldn't be
confused.

Locks do more than just guarding non-atomic invariants though - they
have the acquire/release semantics which make volatility unnecessary.

To be absolutely clear on this, if I have:

int someValue;
object myLock;

....

lock (myLock)
{
int x = someValue;
someValue = x+1;
}

then the read of someValue *cannot* be from a cache - it *must* occur
after the lock has been taken out. Likewise before the lock is
released, the write back to someValue *must* have been made effectively
flushed (it can't occur later than the release in the logical memory
model).

Here's how that's guaranteed by the spec:

"Acquiring a lock (System.Threading.Monitor.Enter or entering a
synchronized method) shall implicitly perform a volatile read
operation"

and

"A volatile read has =3Facquire semantics=3F 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."

That means that the volatile read due to the lock is guaranteed to
occur prior to the "reference to memory" (reading someValue) which
occurs later in the CIL instruction sequence.

The same thing happens the other way round for releasing the lock.
Calling Monitor.Enter/Minitor.Exit is a pretty heavy-weight means of
ensuring acquire semantics; at least 5 times slower if volatile is all you
need.

But still fast enough for almost everything I've ever needed to do, and
I find it a lot easier to reason about a single way of doing things
than having multiple ways for multiple situations. Just a personal
preference - but it definitely *is* safe, without ever needing to
declare anything volatile.
 
W

Willy Denoyette [MVP]

Peter Ritchie said:
Do you think the following is suspicous:?

volatile int intMember;

...assumes you didn't read my last post, I suppose :)

-- Peter

Yes, I do, maybe it's a sign that someone is trying to write lock free
code....

But , I get even more suspicious is when I see this:

....
volatile int intMember;
....
void Foo()
{
lock(myLock)
{
// use intMember here and protect it's shared state by preventing
other threads to touch intMember
// for the duration of the critical section
}
...
}

In above case, when you apply a consistent locking policy to protect your
invariants, there is no need for a volatile intMember. Else, it can be an
indication that some one is trying to play smart, by not taking a lock to
access intMember.


Willy.
 
W

Willy Denoyette [MVP]

Jon Skeet said:
It's not a case of using a lock on a particular value - taking the lock
out creates a memory barrier beyond which *no* reads can pass, not just
reads on the locked expression.


No, it *doesn't* leave a bug - you've misunderstood the effect of lock
having acquire semantics.


Atomicity and volatility are very different things, and shouldn't be
confused.

Locks do more than just guarding non-atomic invariants though - they
have the acquire/release semantics which make volatility unnecessary.

To be absolutely clear on this, if I have:

int someValue;
object myLock;

...

lock (myLock)
{
int x = someValue;
someValue = x+1;
}

then the read of someValue *cannot* be from a cache - it *must* occur
after the lock has been taken out. Likewise before the lock is
released, the write back to someValue *must* have been made effectively
flushed (it can't occur later than the release in the logical memory
model).

Actually on modern processors (others aren't supported anyway, unless you
are running W98 on a 80386) , the read and writes will come/go from/to the
cache (L1, L2 ..), the cache coherency protocol will guarantee consistency
across the cache lines holding the variable has changed. That way, the
"software" has a uniform view of what is called the "memory" irrespective
the number of HW threads (not talking about NUMA here!).

Here's how that's guaranteed by the spec:

"Acquiring a lock (System.Threading.Monitor.Enter or entering a
synchronized method) shall implicitly perform a volatile read
operation"

and

"A volatile read has =3Facquire semantics=3F 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."

That means that the volatile read due to the lock is guaranteed to
occur prior to the "reference to memory" (reading someValue) which
occurs later in the CIL instruction sequence.

The same thing happens the other way round for releasing the lock.


But still fast enough for almost everything I've ever needed to do, and
I find it a lot easier to reason about a single way of doing things
than having multiple ways for multiple situations. Just a personal
preference - but it definitely *is* safe, without ever needing to
declare anything volatile.

Probably one of the reasons why I've never seen a volatile modifier on a
field in the FCL.
And to repeat myself, volatile is not a guarantee against re-ordering and
write buffering by CPU's implementing a weak memory model, like the IA64.
Volatile serves only one thing, that is, prevent optimizations like
re-registering and re-ordering as there would be done by the JIT compiler.

Willy.
 
J

Jon Skeet [C# MVP]

Actually on modern processors (others aren't supported anyway, unless you
are running W98 on a 80386) , the read and writes will come/go from/to the
cache (L1, L2 ..), the cache coherency protocol will guarantee consistency
across the cache lines holding the variable has changed. That way, the
"software" has a uniform view of what is called the "memory" irrespective
the number of HW threads (not talking about NUMA here!).

Yes - I've been using "cache" here somewhat naughtily (because it's the
terminology Peter was using). The sensible way to talk about it is in
terms of the .NET memory model, which is
Probably one of the reasons why I've never seen a volatile modifier on a
field in the FCL.
And to repeat myself, volatile is not a guarantee against re-ordering and
write buffering by CPU's implementing a weak memory model, like the IA64.
Volatile serves only one thing, that is, prevent optimizations like
re-registering and re-ordering as there would be done by the JIT compiler.

No, I disagree with that. Volatile *does* prevent (some) reordering and
write buffering as far as the visible effect to the code is concerned,
whether the effect comes from the JIT or the CPU. Suppose variables a
and b are volatile, then:

int c = a;
int d = b;

will guarantee that the visible effect is the value of "a" being read
before the value of "b" (which wouldn't be the case if they weren't
volatile). In particular, if the variables both start out at 0, then we
do:

b = 1;
a = 1;

in parallel with the previous code, then you might get c=d=1, or c=d=0,
or c=0, d=1, but you're guaranteed *not* to get c=1, d=0.

Whether that involves the JIT doing extra work to get round a weak CPU
memory model is unimportant - if it doesn't prevent that last
situation, it's failed to meet the spec.
 
W

Willy Denoyette [MVP]

Jon Skeet said:
Yes - I've been using "cache" here somewhat naughtily (because it's the
terminology Peter was using). The sensible way to talk about it is in
terms of the .NET memory model, which is


No, I disagree with that. Volatile *does* prevent (some) reordering and
write buffering as far as the visible effect to the code is concerned,
whether the effect comes from the JIT or the CPU. Suppose variables a
and b are volatile, then:

int c = a;
int d = b;

will guarantee that the visible effect is the value of "a" being read
before the value of "b" (which wouldn't be the case if they weren't
volatile). In particular, if the variables both start out at 0, then we
do:

b = 1;
a = 1;

in parallel with the previous code, then you might get c=d=1, or c=d=0,
or c=0, d=1, but you're guaranteed *not* to get c=1, d=0.

Whether that involves the JIT doing extra work to get round a weak CPU
memory model is unimportant - if it doesn't prevent that last
situation, it's failed to meet the spec.
 
W

Willy Denoyette [MVP]

Jon Skeet said:
Yes - I've been using "cache" here somewhat naughtily (because it's the
terminology Peter was using). The sensible way to talk about it is in
terms of the .NET memory model, which is


No, I disagree with that. Volatile *does* prevent (some) reordering and
write buffering as far as the visible effect to the code is concerned,
whether the effect comes from the JIT or the CPU. Suppose variables a
and b are volatile, then:

int c = a;
int d = b;

will guarantee that the visible effect is the value of "a" being read
before the value of "b" (which wouldn't be the case if they weren't
volatile). In particular, if the variables both start out at 0, then we
do:

b = 1;
a = 1;

in parallel with the previous code, then you might get c=d=1, or c=d=0,
or c=0, d=1, but you're guaranteed *not* to get c=1, d=0.

Whether that involves the JIT doing extra work to get round a weak CPU
memory model is unimportant - if it doesn't prevent that last
situation, it's failed to meet the spec.


Agreed, reads (all or not volatile) cannot move before a volatile read, and
writes cannot move after a volatile write.

But this is not my point, what I'm referring to is the following (assuming a
and b are volatile):

a = 5;
int d = b;

here it's allowed for the write to move after the read, they are referring
to different locations and they have no (visible) dependencies).


Willy.
 
G

Guest

For the record, I've been talking about the compiler re-organizing the code
during optimization. And I thought I was pretty clear about the compiler
"caching" values to a register, not the CPUs caches.
It's not a case of using a lock on a particular value - taking the lock
out creates a memory barrier beyond which *no* reads can pass, not just
reads on the locked expression.

I don't see you how you get that from:
"Acquiring a lock (System.Threading.Monitor.Enter or entering a
synchronized method) shall implicitly perform a volatile read
operation"

and

"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."

I would agree that a volatile read/write is performed on the parameter for
Monitor.Enter and Monitor.Exit.
To be absolutely clear on this, if I have:

int someValue;
object myLock;

....

lock (myLock)
{
int x = someValue;
someValue = x+1;
}

then the read of someValue *cannot* be from a cache - it *must* occur
after the lock has been taken out. Likewise before the lock is
released, the write back to someValue *must* have been made effectively
flushed (it can't occur later than the release in the logical memory
model).

You're talking about CPU re-organizations and CPU cachings, I've been
talking about compiler optimizations.

None of the quotes affect code already optimized by the compiler. If the
compiler decides writing code that doesn't write a temporary value directly
back to the member/variable because it's faster and it doesn't know it's
volatile, nothing you've quoted will have a bearing on that.

Monitor.Enter may create memory barrier for the current thread, it's unclear
from 335; but it could not have affected code that accesses members outside
of a lock block.

335 says nothing about what the compiler does with code within a locked block.
 
J

Jon Skeet [C# MVP]

Peter Ritchie said:
For the record, I've been talking about the compiler re-organizing the code
during optimization. And I thought I was pretty clear about the compiler
"caching" values to a register, not the CPUs caches.

That's all irrelevant - the important thing is the visible effect.

You're talking about CPU re-organizations and CPU cachings, I've been
talking about compiler optimizations.

As I said to Willy, I shouldn't have used the word "cache". Quite what
could make things appear to be out of order is irrelevant - they're all
forbidden by the spec in this case.
None of the quotes affect code already optimized by the compiler. If the
compiler decides writing code that doesn't write a temporary value directly
back to the member/variable because it's faster and it doesn't know it's
volatile, nothing you've quoted will have a bearing on that.

So here are you talking about the C# compiler rather than the JIT
compiler?

If so, I agree there appears to be a hole in the C# spec. I don't
believe the C# compiler *will* move any reads/writes around, however.
For the rest of the post, however, I'll assume you were actually still
talking about the JIT.
Monitor.Enter may create memory barrier for the current thread, it's unclear
from 335; but it could not have affected code that accesses members outside
of a lock block.

Agreed, but irrelevant.
335 says nothing about what the compiler does with code within a locked block.

Agreed, but irrelevant.

The situation I've been talking about is where a particular variable is
only referenced *inside* lock blocks, and where all the lock blocks
which refer to that variable are all locking against the same
reference.

At that point, there is an absolute ordering in terms of the execution
of those lock blocks - only one can execute at a time, because that's
the main point of locking.

Furthermore, while the ordering *within* the lock can be moved, none of
the reads which are inside the lock can be moved to before the lock is
acquired (in terms of the memory model, however that is achieved) and
none of the writes which are inside the lock can be moved to after the
lock is released.

Therefore any change to the variable is seen by each thread, with no
"stale" values being involved.

Now I totally agree that *if* you start accessing the variable from
outside a lock block, all bets are off - but so long as you keep
everything within locked sections of code, all locked with the same
lock, you're fine.
 
J

Jon Skeet [C# MVP]

Agreed, reads (all or not volatile) cannot move before a volatile read, and
writes cannot move after a volatile write.

But this is not my point, what I'm referring to is the following (assuming a
and b are volatile):

a = 5;
int d = b;

here it's allowed for the write to move after the read, they are referring
to different locations and they have no (visible) dependencies).

Assuming they're not volatile, you're absolutely right - but I thought
you were talking about what could happen with *volatile* variables,
given that you said:

<quote>
And to repeat myself, volatile is not a guarantee against re-ordering
and write buffering by CPU's implementing a weak memory model, like the
IA64.
</quote>

I believe volatile *is* a guarantee against the reordering of volatile
operations. Volatile isn't a guarantee against the reordering of two
non-volatile operations with no volatile operation between them, but
that's the case for the JIT as well as the CPU.

I don't believe it's necessary to talk about the JIT separately from
the CPU when thinking on a purely spec-based level. If we were looking
at generated code we'd need to consider the platform etc, but at a
higher level than that we can just talk about the memory model that the
CLR provides, however it provides it.
 
W

Willy Denoyette [MVP]

Jon Skeet said:
Assuming they're not volatile, you're absolutely right - but I thought
you were talking about what could happen with *volatile* variables,
given that you said:

<quote>
And to repeat myself, volatile is not a guarantee against re-ordering
and write buffering by CPU's implementing a weak memory model, like the
IA64.
</quote>

I believe volatile *is* a guarantee against the reordering of volatile
operations. Volatile isn't a guarantee against the reordering of two
non-volatile operations with no volatile operation between them, but
that's the case for the JIT as well as the CPU.

I don't believe it's necessary to talk about the JIT separately from
the CPU when thinking on a purely spec-based level. If we were looking
at generated code we'd need to consider the platform etc, but at a
higher level than that we can just talk about the memory model that the
CLR provides, however it provides it.
 
W

Willy Denoyette [MVP]

Jon Skeet said:
Assuming they're not volatile, you're absolutely right - but I thought
you were talking about what could happen with *volatile* variables,
given that you said:

Note really, I'm talking about volatile field b
The (ECMA) rules for volatile state that:
- reads and writes cannot move before a *volatile* read
- reads and writes cannot move after a *volatile* write.
As I see it, this means that ordinary writes can move after a volatile read.
So, in the above, the write to 'a' can move after the volatile read from
'b', agree?
However, above rules are not clear on the case where 'a' and 'b' are
volatile, do the rules prohibit a volatile write to move after a volatile
read? IMO they don't.
 
J

Jon Skeet [C# MVP]

Willy Denoyette said:
Note really, I'm talking about volatile field b

Sorry - I stupidly misread "are volatile" as "are not volatile". Doh!
The (ECMA) rules for volatile state that:
- reads and writes cannot move before a *volatile* read
- reads and writes cannot move after a *volatile* write.
As I see it, this means that ordinary writes can move after a volatile read.
So, in the above, the write to 'a' can move after the volatile read from
'b', agree?
However, above rules are not clear on the case where 'a' and 'b' are
volatile, do the rules prohibit a volatile write to move after a volatile
read? IMO they don't.

Yup, I think you're right.

I basically think of volatile as pretty much *solely* a way to make
sure you always see the latest value of the variable in any thread.
When it comes to interactions like that, while they're interesting to
reason about, I'd rather use a lock in situations where I really care
:)
 
W

Willy Denoyette [MVP]

Sorry , but previous message went out before being finished.



Willy Denoyette said:
Note really, I'm talking about volatile field b
The (ECMA) rules for volatile state that:
- reads and writes cannot move before a *volatile* read
- reads and writes cannot move after a *volatile* write.
As I see it, this means that ordinary writes can move after a volatile
read.
So, in the above, the write to 'a' can move after the volatile read from
'b', agree?
However, above rules are not clear on the case where 'a' and 'b' are
volatile, do the rules prohibit a volatile write to move after a volatile
read? IMO they don't.

However, the memory model as implemented by V2 of the CLR, also defines an
explicit rule that states that :
- All shared writes shall have release semantics.
which could be restated as : "writes cannot be reordered, point". That means
that on the current platforms, emitting every write with release semantics
is sufficient to:
1) perform each processor's stores in order, and
2) make them visible to other processors in that order
That makes the execution environment Processor Consistent (PC), great, that
would mean that the above optimization (move of the write after the volatile
read) is excluded. The problem however is, that notably the JIT64 on IA64,
does not enforce that rule consistently, it appears to enable such
optimizations in violation of the "managed memory model". MSFT is aware of
this, but as of today, I have no idea whether they are addressing this or
that they consider this to be acceptable on the IA64 platform.

Willy.
 
J

Jon Skeet [C# MVP]

However, the memory model as implemented by V2 of the CLR, also defines an
explicit rule that states that :
- All shared writes shall have release semantics.
which could be restated as : "writes cannot be reordered, point". That means
that on the current platforms, emitting every write with release semantics
is sufficient to:
1) perform each processor's stores in order, and
2) make them visible to other processors in that order
That makes the execution environment Processor Consistent (PC), great, that
would mean that the above optimization (move of the write after the volatile
read) is excluded. The problem however is, that notably the JIT64 on IA64,
does not enforce that rule consistently, it appears to enable such
optimizations in violation of the "managed memory model". MSFT is aware of
this, but as of today, I have no idea whether they are addressing this or
that they consider this to be acceptable on the IA64 platform.

I *thought* (though I could well be wrong) that before release, the
IA64 JIT was indeed very lax, but that it had been tightened up close
to release. I wouldn't like to try to find any evidence of that though
;)

Just another reason to stick to "simple" thread safety via locks, IMO.
 
G

Guest

Jon Skeet said:
So here are you talking about the C# compiler rather than the JIT
compiler?

If so, I agree there appears to be a hole in the C# spec. I don't
believe the C# compiler *will* move any reads/writes around, however.
For the rest of the post, however, I'll assume you were actually still
talking about the JIT.

Could be either, I suppose. I don't think the spec. is clear at all in this
respect. With regard to compiler-level optimzations, 12.6.4 details: "...are
visible in the order specified in the CIL.". Which suggests to me that the
C#-to-IL compiler doesn't optimize other than potential reorganizations. The
detail before that seems concerning: "guarentees, within a single thread of
execution, that side-effects ... are visble in the order specified by the
CIL". Sounds like memory barriers are set up within Monitor.Enter and
Monitor.Exit to ensure CPU-level re-ordering is limited; but, unless there's
a modreq(volatile) on a member the JIT can't know not to introduce
cross-thread visible side-effects unless it looks for calls to Monitor.Enter
and Monitor.Exit.
 
J

Jon Skeet [C# MVP]

Peter Ritchie said:
Could be either, I suppose. I don't think the spec. is clear at all in this
respect. With regard to compiler-level optimzations, 12.6.4 details: "...are
visible in the order specified in the CIL.". Which suggests to me that the
C#-to-IL compiler doesn't optimize other than potential reorganizations.

The CIL spec can't determine what the C# compiler is allowed to do. I
haven't seen anything in the C# spec which says it won't reorder things
- although I hope and believe that it wohn't.
The detail before that seems concerning: "guarentees, within a single thread of
execution, that side-effects ... are visble in the order specified by the
CIL". Sounds like memory barriers are set up within Monitor.Enter and
Monitor.Exit to ensure CPU-level re-ordering is limited; but, unless there's
a modreq(volatile) on a member the JIT can't know not to introduce
cross-thread visible side-effects unless it looks for calls to Monitor.Enter
and Monitor.Exit.

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.

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.

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.
 
G

Guest

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).
 
J

Jon Skeet [C# MVP]

Peter Ritchie said:
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).

That's irrelevant though - the spec just says what will happen, not
which bit is responsible for making sure it happens.
Again, run-time operations.

But affected by compilation decisions.
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);
}
}
}

That is indeed buggy code - you're accessing number without locking.
That's not the situation I've been describing. If you change your code
to:

int count = random.Next();
for (int i = 0; i < count; ++i) {
lock (locker)
{
number++;
Trace.WriteLine(number);
}
}

then the code is okay. That's the situation I've been consistently
describing.

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).

Again, you're making assumptions about which bit of the spec applies to
CPU optimisations and which bit applies to JIT compilation
optimisations. The spec doesn't say anything about that - it just makes
guarantees about what will be visible when. With the corrected code
above, there is no bug, because the JIT must know that it *must*
freshly read number after acquiring the lock, and *must* "flush" number
to main memory before releasing the lock.
 
G

Guest

:
<snip>

I guess we'll just have to disagree on a few things, for the reasons I've
already stated. I don't see much point in going back and forth saying the
same things...

With regard to runtime volatile read/writes and acquire/release semantics of
Monitor.Enter and Monitor.Exit we can agree.

I don't agree that anything specified in either 334 or 335 covers all levels
of potential compile-time class member JIT/IL compiler optimizations.

I don't agree that "int number; void UpdateNumber(){lock(locker){
number++;}}" is equally as safe as "volatile int number; void UpdateNumber(){
number++; }"

With the following Monitor.Enter/Exit IL, for example:
..field private int32 number
..method private hidebysig instance void UpdateNumber() cil managed
{
.maxstack 3
.locals init (
[0] int32 count,
[1] int32 i)
L_0000: ldarg.0
L_0001: ldfld class [mscorlib]System.Random Tester::random
L_0006: callvirt instance int32 [mscorlib]System.Random::Next()
L_000b: stloc.0
L_000c: ldarg.0 // *
L_000d: ldfld object Tester::locker //*
L_0012: call void [mscorlib]System.Threading.Monitor::Enter(object) //*
L_0017: ldc.i4.0
L_0018: stloc.1
L_0019: br.s L_003d
L_001b: ldarg.0
L_001c: dup
L_001d: ldfld int32 Tester::number
L_0022: ldc.i4.1
L_0023: add
L_0024: stfld int32 Tester::number
L_0029: ldarg.0
L_002a: ldfld int32 Tester::number
L_002f: box int32
L_0034: call void [System]System.Diagnostics.Trace::WriteLine(object)
L_0039: ldloc.1
L_003a: ldc.i4.1
L_003b: add
L_003c: stloc.1
L_003d: ldloc.1
L_003e: ldloc.0
L_003f: blt.s L_001b
L_0041: leave.s L_004f
L_0043: ldarg.0 // *
L_0044: ldfld object Tester::locker // *
L_0049: call void [mscorlib]System.Threading.Monitor::Exit(object) //*
L_004e: endfinally
L_004f: ret
.try L_0017 to L_0043 finally handler L_0043 to L_004f
}

....what part of that IL tells the JIT/IL compiler that Tester.number
specifically should be treated differently--where lines commented // * are
the only lines distinct to usage of Monitor.Enter/Exit?

Compared to use of volatile:
..field private int32
modreq([mscorlib]System.Runtime.CompilerServices.IsVolatile) number
..method private hidebysig instance void UpdateNumber() cil managed
{
.maxstack 3
.locals init (
[0] int32 count,
[1] int32 i)
L_0000: ldarg.0
L_0001: ldfld class [mscorlib]System.Random One.Tester::random
L_0006: callvirt instance int32 [mscorlib]System.Random::Next()
L_000b: stloc.0
L_000c: ldc.i4.0
L_000d: stloc.1
L_000e: br.s L_0038
L_0010: ldarg.0
L_0011: dup
L_0012: volatile
L_0014: ldfld int32
modreq([mscorlib]System.Runtime.CompilerServices.IsVolatile)
One.Tester::number
L_0019: ldc.i4.1
L_001a: add
L_001b: volatile
L_001d: stfld int32
modreq([mscorlib]System.Runtime.CompilerServices.IsVolatile)
One.Tester::number
L_0022: ldarg.0
L_0023: volatile
L_0025: ldfld int32
modreq([mscorlib]System.Runtime.CompilerServices.IsVolatile)
One.Tester::number
L_002a: box int32
L_002f: call void [System]System.Diagnostics.Trace::WriteLine(object)
L_0034: ldloc.1
L_0035: ldc.i4.1
L_0036: add
L_0037: stloc.1
L_0038: ldloc.1
L_0039: ldloc.0
L_003a: blt.s L_0010
L_003c: ret
}

....where an IL compiler is given ample amounts of information that
Tester.number should be treated differently.

I don't think it's safe, readable, or future friendly to utilize syntax
strictly for their secondary consequences (using Monitor.Enter/Exit not for
synchronization but for acquire/release semantics. As in the above line
where modification of an int is already atomic; "synchronization" is
irrelevant), even if they were effectively identical to another syntax. Yes,
if you've got a non-atomic invariant you still have to synchronize (with
lock, etc.)... but volatility is different and needs to be accounted for
equally as much as thread-safety.

-- Peter
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top