Yes. No. Maybe.
The formally correct answer is: This is not safe.
The practical answer is not that easy. It's something like "This is safe, kind of, under some conditions".
Reads (any number of them) in absence of concurrent writes are always safe. Reads (even a single one) in presence of concurrent writes (even a single one) are formally never safe, but they are atomic on most processors in most situations, and this can be just good enough. Changing values (like incrementing a counter) is nearly always troublesome, even in practice, without explicitly using atomic operations.
Atomicity
The C++ standard mandates that you use std::atomic
or one of its specializations (or higher level synchronization primitives), or you are doomed. Demons will fly out of your nose (no, they won't... but as far as the standard goes, they might as well).
All real, non-theoretical CPUs access memory exclusively via cache lines except in very special conditions which you must expclicitly provoke (such as using write-combining instructions). An entire cache line can be read or written to, atomically, at a time -- never anything different. Reading any memory location that is being written to might not give the value that you expect (if it has been updated in the mean time), but it will never return a "garbage" value.
Now of course a variable might cross a cacheline, in which case access isn't atomic, but unless you deliberately provoke it, this will not happen (since integral variables are power-of-two sized such as 2, or 4, or 8, and cache lines are also power-of-two sized and larger such as 64 or 128 -- if your variables are properly aligned to the former as by default, they are automatically also completely contained within the latter. Always.).
Ordering
Although your reads (and writes) may be atomic, and you might say that you only care whether some flag is zero or not so who cares even if a value is garbled, you don't have a guarantee that things happen in the order that you expect!
The "normal" expectation that if you say that A happens before B, then A indeed happens before B and A can be seen by someone else before B is generally not true. In other words, it is perfectly possible that your worker thread prepares some data and then sets the ready
flag. Your main thread sees that the ready
flag is set, and begins reading some random garbage while the real data is still on its way somewhere in the cache hierarchy. Or maybe half of it is visible to the main thread already, but the other half isn't.
For this, C++11 introduced the concept of memory order. This means no more and no less than besides having the guarantee of atomicity, you also have a way of requesting a happens-before guarantee.
Most of the time, this only prevents the compiler from moving around loads and stores, but on some architectures, it may cause special instructions to be emitted (that's not your problem, though).
Read-Modify-Write
This is a particularly nefarious one. A simple thing like ++flag;
can be desastrous. This is not at all the same as flag = 1;
Without using proper atomic instructions, this is never safe, as it involves (atomically) reading, then modifying, and then (atomically) writing a cache line.
The problem is, while reading and writing are both atomic, the whole thing isn't. Nor is there any guarantee about ordering.
Solution?
Either use std::atomic
or block on a condition variable. The former will involve spinning, which may or may not be detrimental (depending on the frequency and latency requirements) while the latter will be CPU conservative.
You could use a mutex
to synchronize access to the global variable, too, but if you involve a heavyweight primitive, you might as well go for the condition variable instead of spinning (which will be the "correct" approach).