views:

855

answers:

7

If I have a multi-threaded program that reads a cache-type memory by reference. Can I change this pointer by the main thread without risking any of the other threads reading unexpected values.

As I see it, if the change is atomic the other threads will either read the older value or the newer value; never random memory (or null pointers), right?

I am aware that I should probably use synchronisation methods anyway, but I'm still curious.

Are pointer changes atomic?

Update: My platform is 64-bit Linux (2.6.29), although I'd like a cross-platform answer as well :)

+10  A: 

The C language says nothing about whether any operations are atomic. I've worked on microcontrollers with 8 bit buses and 16-bit pointers; any pointer operation on these systems would potentially be non-atomic. I think I remember Intel 386s (some of which had 16-bit buses) raising similar concerns. Likewise, I can imagine systems that have 64-bit CPUs, but 32-bit data buses, which might then entail similar concerns about non-atomic pointer operations. (I haven't checked to see whether any such systems actually exist.)

EDIT: Michael's answer is well worth reading. Bus size vs. pointer size is hardly the only consideration regarding atomicity; it was simply the first counterexample that came to mind for me.

Dan Breslau
so the pointer could be half loaded at a context switch?
ojblass
@ojblass: Such things have happened in older architectures; they might happen even with newer ones. (See my updated comment.)
Dan Breslau
You learn something each and every day... thanks...
ojblass
@ojblass, when working with C/C++ i like to take the stance that anything is possible until the standard and compiler documentation says it's not so. It's the only way to be safe :)
JaredPar
The pointer read/write might not be atomic from a bus perspective, but if it's a single instruction, then it will likely be atomic from the CPU's perspective. Strange architectures probably don't do any sort of multiprocessing, so it's *usually* fine. Usually. Fingers crossed.
tc.
@tc: The behavior of single instructions is still platform-specific. I know there have been _some_ CPUs (though I don't remember which ones) in which interrupts could be processed in the middle of an instruction. All bets are off when that happens.
Dan Breslau
+5  A: 

You didn't mention a platform. So I think a slightly more accurate question would be

Are pointer changes guaranteed to be atomic?

The distinction is necessary because different C/C++ implementations may vary in this behavior. It's possible for a particular platform to guarantee atomic assignments and still be within the standard.

As to whether or not this is guaranteed overall in C/C++, the answer is No. The C standard makes no such guarantees. The only way to guarantee a pointer assignment is atomic is to use a platform specific mechanism to guarantee the atomicity of the assignment. For instance the Interlocked methods in Win32 will provide this guarantee.

Which platform are you working on?

JaredPar
+2  A: 

The cop-out answer is that the C spec does not require a pointer assignment to be atomic, so you can't count on it being atomic.

The actual answer would be that it probably depends on your platform, compiler, and possibly the alignment of the stars on the day you wrote the program.

Eric Petroelje
That's not a cop-out; it's the right answer. If you're using concurrency you *need* to have it right; otherwise you waste even more time trying to replicate a one-in-a-bazillion heisenbug.
Alex Feinman
+1  A: 

The only thing guaranteed by the standard is the sig_atomic_t type.

As you've seen from the other answers, it is likely to be OK when targeting generic x86 architecture, but very risky with more "specialty" hardware.

If you're really desperate to know, you can compare sizeof(sig_atomic_t) to sizeof(int*) and see what they are you your target system.

Clyde
+12  A: 

As others have mentioned, there is nothing in the C language that guarantees this, and it is dependent on your platform.

On most contemporary desktop platforms, the read/write to a word-sized, aligned location will be atomic. But that really doesn't solve your problem, due to processor and compiler re-ordering of reads and writes.

For example, the following code is broken:

Thread A:

DoWork();
workDone = 1;

Thread B:

while(workDone != 0);

ReceiveResultsOfWork();

Although the write to workDone is atomic, on many systems there is no guarantee by the processor that the write to workDone will be visible to other processors before writes done via DoWork() are visible. The compiler may also be free to re-order the write to workDone to before the call to DoWork(). In both cases, ReceiveResultsOfWork() might start working on incomplete data.

Depending on your platform, you may need to insert memory fences and so on to ensure proper ordering. This can be very tricky to get right.

Or just use locks. Much simpler, much easier to verify as correct, and in most cases more than performant enough.

Michael
Actually, I don't think a C (or C++) compiler can re-order workDone = 1 to BEFORE a function call that precedes it, unless it knows something else about workDone (like the never-approved "noalias" keyword). On the other hand, if you had foo = bar; qwerty = uiop; workDone = 1, then workDone = 1 could move (or even temporarily be a register-only value, if workDone is not volatile). If ReceiveResultsOfWork looks at qwerty or foo, it might not get uiop or bar, even though workDone is 1. I'd have to check what happens if DoWork() ends up inlined.
jesup
The compiler can do that reorder if it can prove that DoWork doesn't access workDone in any defined manner. This may actually happen if DoWork is small enough and in the same translation unit and the compiler decides to inline it.
derobert
The important thing is the compiler isn't bound by the standard to ensure workDone is written to after the call to DoWork(). In practice, you are definitely correct - if DoWork() is a real function call the compiler prbably won't re-order. If DoWork is inlined, workDone could easily be re-ordered to before DoWork() or during it.
Michael
Although it's a C++ keyword, most C compilers honor the volatile modifier, which would prevent the compiler from reordering writes to the variable, and almost guarantee that the memory is written through cache. I say almost because nothing is guaranteed in C, but its the convention of every major C compiler.
Jeff Mc
I don't believe volatile on its own can prevent reordering, it just forces the compiler to refetch on read and to not optimize away writes. If DoWork() could be shown not to modify workDone() and not access any other volatiles, the compiler could still be free to re-order workDone = 1, even if it were volatile.Of course, compilers can add more meaning to the volatile keyword, and I have heard of some platforms that extend volatile to have meaning for re-ordering. But I don't believe the standard guarantees this.
Michael
A compiler isn't allowed to re-order DoWork and the assignment of WorkDone, because calling DoWork() introduces a sequence point. Also if WorkDone is declared volatile (which it has to be for the code to work), there is another sequence point any time it is accessed. The order of operations is guaranteed in this case.
Don
@Michael, Visual Studio compiler is probably the one you heard about, it assigns acquire and release semantics to volatile reads and writes respectively.
avakar
@Don: Memory fences aren't designed to prevent reordering in the compiler, they're designed to prevent reordering in the CPU.
Ben Voigt
@Don: the compiler must preserve *apparent order* in a single-threaded app with no signal handlers (signal handlers are possibly trickier than threads...). Once you have threads, you need to look to your threading library; see pthread_barrier_* and similar.
tc.
@Ben: Memory fences also prevent reordering by the compiler in the case `busy++; do { /* stuff */ } while (!done); busy--;`. If busy is non-volatile and /*stuff*/ doesn't touch busy the compiler can assume that the ++ and -- cancel each other out.
tc.
+1  A: 

It turns out to be quite a complex question. I asked a similar question and read everything I got pointed to. I learned a lot about how caching works in modern architectures, and didn't find anything that was definitive. As others have said, if the bus width is smaller than the pointer bit-width, you might be in trouble. Specifically if the data falls across a cache-line boundary.

A prudent architecture will use a lock.

Leonard
+2  A: 

'normal' pointer modification isn't guaranteed to be atomic.

check 'Compare and Swap' (CAS) and other atomic operations, not a C standard, but most compilers have some access to the processor primitives. in the GNU gcc case, there are several built-in functions

Javier