Re: volatile and win32 multithreading
From: Doug Harrison [MVP] (dsh_at_mvps.org)
Date: 01/25/05
- Next message: Douglas Peterson: "Re: Overloading class new and delete operators with parameters"
- Previous message: Andre Gückel: "Re: Parallel port accesses _outp"
- In reply to: Michael K. O'Neill: "Re: volatile and win32 multithreading"
- Next in thread: Michael K. O'Neill: "Re: volatile and win32 multithreading"
- Reply: Michael K. O'Neill: "Re: volatile and win32 multithreading"
- Messages sorted by: [ date ] [ thread ]
Date: Tue, 25 Jan 2005 10:37:03 -0600
Michael K. O'Neill wrote:
>"Carl Daniel [VC++ MVP]" <cpdaniel_remove_this_and_nospam@mvps.org.nospam>
>wrote in message news:eaJlbViAFHA.3824@TK2MSFTNGP10.phx.gbl...
>
>...
>> volatile is neither necessary nor sufficient for writing portable code
>that
>> shares variables between threads.
>>
>
>I agree that "volatile" is not sufficient for multithreaded code (and that
>proper synchronization as you described is critical), but are you really
>sure that's it's also not "necessary"?
>
>As I understand it, "volatile" is a signal to optimizing compilers,
>cautioning that they should not optimize too much (such as by re-ordering
>operations or preloading into registers). Such optimizations often cause
>difficulties while multi-threading. So, isn't it safer to use "volatile"
>for a shared resource?
Suppose your shared resource is a std::vector. Declare it volatile. Now try
to call member functions on it. You don't get very far, do you? How do you
fix it? Cast volatile away? If you do, then you're back in the same boat as
you were when you didn't declare it volatile, except your code is now even
more undefined, because you're accessing a volatile object through a
non-volatile lvalue. No one I know writes volatile versions of member
functions, so if volatile is necessary for MT programs, practically no C++
class ever written can be used in a thread-safe way without a massive,
intrusive rewrite.
What about simple types like int? While volatile will preserve ordering at
the compiler level of operations on volatile variables, it's typically
implemented such that it has no effect at the hardware level, which makes it
insufficient for SMP systems such as Itanium. (That is, it doesn't have
memory barrier semantics.) Here's an excerpt (mildly edited) from an earlier
message I wrote about the uses of this sort of volatile.
***** begin excerpt
I can think of four valid uses for volatile, all more or less related to the
idea that volatile suppresses optimizations and forces the compiler to go to
memory for each read and write to a volatile object.
1. On amenable hardware like x86, this idiom can work, and it requires
volatile to prevent the compiler from caching x in a register in the loop:
// The variable x is accessible to multiple threads, one or more of
// which change it and indirectly stop the loop below, which is
// executing in another thread. Note that x must always be accessed
// atomically for this sort of thing to work in general.
volatile bool x = true;
while (x)
whatever;
// Assuming everyone observes the locking protocol, the following approach
// would work on all hardware, even weird architectures like Sparc,
// Itanium, and others which require memory barriers. You could also use
// InterlockedXXX on Windows instead of a mutex, though that does
// limit the type of the variable x to LONG.
bool x = true; // As above, just non-volatile
for (;;)
{
lock(mx);
bool y = x;
unlock(mx);
if (!y)
break;
whatever;
}
2. You need volatile to avoid undefined behavior in certain signal handler
and setjmp/longjmp scenarios.
3. You can use volatile, say, to declare a pointer to a volatile int, which
represents a memory location updated at the interrupt level, something you
can't synchronize with, and which is outside the compiler's knowledge of the
program.
4. You can use volatile to prevent the compiler from optimizing away
operations which, as far as the compiler is concerned, seem to have no
purpose, because it can't see that the results are used or that the side
effects of computing the results are important, e.g. a timing loop.
***** end excerpt
Here's an excerpt (mildly edited) from a message I wrote about compiler
optimizations, which explains why a goodly amount of correct behavior comes
for free, at least in Windows, where the locking operations invoke functions
in opaque DLLs. It talks about global variables, but it can also apply to
local variables that have been passed by reference (or address) to other
functions.
***** begin excerpt
Here's a simplistic explanation which is probably not far off the mark
for VC. The mutex lock/unlock operations are function calls, and the
compiler knows nothing about these functions, so it can't do any
interprocedural optimization. Global variables are reachable through
functions called by the current function, including lock/unlock. The
compiler can't see into the lock/unlock functions to determine that they
don't access the globals or call other functions which ultimately do access
them. Thus, when you have the sequence below, for non-volatile, global
variables x and y:
m.lock();
y = x;
x = 2;
m.unlock();
The compiler cannot optimize the assignment to x out of existence, because
it can't tell that unlock() won't refer to x. It can't move the y and x
assignments before or after the lock/unlock calls, because that can change
the values those functions observe. It can't cache the value of x, call
lock(), and assign the cached value to y, because lock() may have modified
x. Before calling unlock(), it must flush x and y out of registers to
memory, so that unlock() will observe their current values. And so on. The
only way I know to screw this up is to write to the variables outside of the
critical section, but that's a violation of the locking protocol. So at the
compiler level, the variables don't need to be volatile.
What about code executed strictly inside the critical section? Optimizations
performed there on x and y don't matter, because other threads are observing
the locking protocol, so no one else is accessing them when a thread is
inside a critical section involving them. If another thread, or, say, an OS
interrupt handler is concurrently (and I hope atomically) accessing x or y,
you should hope EVERYone is using the InterlockedXXX functions, because that
will be well-defined, and it won't matter if the variables are declared
volatile or not. But if ANYone is accessing the variables pell-mell, you're
back to indeterminate behavior that depends on the architecture of your
computer, and volatile may or may not be sufficient. It depends on how your
compiler implements it. Compilers typically don't confer memory barrier
semantics on volatile, and this sort of volatile is not sufficient for
machines that require them.
In addition to providing mutual exclusion, the mutex lock/unlock operations
issue whatever memory barrier instructions are necessary, so that the writes
are visible to other threads observing the locking protocol. So at the
hardware level, there's no need for the variables to be volatile, assuming
volatile implies MB instructions (which it currently does not in VC++),
because they're implicit in the mutex lock/unlock operations.
(NB: A compiler which can see into the locking operations might have to mark
them somehow to suppress optimizations which can violate the expected
semantics. There's no other reasonable choice for a compiler intended to be
used for MT programming.)
***** end excerpt
>There have been quite a few (maybe too many) discussions over use of
>"volatile" over at microsoft.public.vc.mfc. Here's one from last November:
>http://groups-beta.google.com/group/microsoft.public.vc.mfc/browse_frm/thread/bcaa8908adbafc7e/66ca9ead549797ac ,
>which mentions a white paper that includes talk about "volatile" in a driver
>environment: http://www.microsoft.com/whdc/driver/kernel/MP_issues.mspx
I agree the MS whitepaper is worth reading. As for the message you linked
to, it gets my early vote for "Most Accidentally Ironic Message of the 21st
Century". :)
-- Doug Harrison Microsoft MVP - Visual C++
- Next message: Douglas Peterson: "Re: Overloading class new and delete operators with parameters"
- Previous message: Andre Gückel: "Re: Parallel port accesses _outp"
- In reply to: Michael K. O'Neill: "Re: volatile and win32 multithreading"
- Next in thread: Michael K. O'Neill: "Re: volatile and win32 multithreading"
- Reply: Michael K. O'Neill: "Re: volatile and win32 multithreading"
- Messages sorted by: [ date ] [ thread ]
Relevant Pages
|