Is it possible to read half-written, corrupt primitive variable when using multithreading in C++?

Viewed 443

If on one thread I would be writing to a primitive-type variable, such as int, and the other would be reading it, would there be a possibility of reading partially modified data, as with more complex data types?

If yes, then would my only rescue be either atomic or mutex or is there a solution with less performance overhead?

2 Answers

In theory (and also real life), yes. You must synchronise access to variables that are read and written by different threads.

Some hardware architectures may not need the synchronization (in which case a competent compiler should just remove it), but some architectures have very lax guarantees (like DEC Alpha - and others) and synchronization is very much required.

For portable and predictable results across implentations, you must synchronise access to variables. Partial reads/writes can happen (usually at 3-o'clock at night at your most important customer when everyone is on vacation).

Not to mention the fact that lacking synchronization, your program contains a data race, and data races are by definition Undefined Behaviour. And once your program contains UB (anywhere), your compiler no longer has any restrictions on what code it may generate (for all of your program). So even if the hardware guarantees it is safe, the compiler may exploit the presence of theoretical UB to perform optimizations that will break your program - and not necessarily where you expect that breakage to happen.

By the wording of the standard yes: accessing the same non-atomic object from more than one thread, where at least one access is a write, is undefined behavior. So not only could you see "half-written" or "corrupt" primitive data, other things are possible such as corruption of unrelated data, infinite loops, your computer becoming sentient and building a time machine and traveling to the past to convince your ancestors never to pursue the activities that would lead to your birth, segmentation faults, noxious gases being emitted from your USB ports, etc.

In practice, I'm not aware of how this could happen on modern architectures when writing aligned values of primitive types, other than "wide" primitive types that aren't natively supported by the hardware (e.g., some multi-socket AMD machines fail to write aligned 128-bit values atomically). Compilers will generally emit full-width loads and stores, so I don't know of a realistic scenario where you'd be tripped up by e.g. a compiler transforming a 32-bit store into two 16-bit stores, although it is perfectly within its rights to.

That doesn't mean you should do it: the compiler can make other assumptions that can trip you up: e.g., that the value never changes and hence it doesn't need to be re-read at all. More importantly, what you may want is std::atomic in concert with std::memory_order_relaxed in key places which often has zero or almost zero performance implications. It's a sharp tool though.

Related