5

If one thread reads a non-atomic primitive variable another thread is writing to, is it guaranteed to read the value either before or after the write or can it somehow read some corrupt version during the write?

I realize collection such as a linked list are another story.

Dmitry Kuzminov
  • 6,180
  • 6
  • 18
  • 40
mczarnek
  • 1,305
  • 2
  • 11
  • 24
  • 2
    Related: https://stackoverflow.com/a/5892468/4123703 It depends on platform (32/64bit). – Louis Go Jun 18 '20 at 01:13
  • 3
    No, it is not guaranteed (at least, not by the standard). That's what atomic variables are for. These are generally lock-free for primitive types, so there is no good reason not to use them. – Paul Sanders Jun 18 '20 at 01:20
  • 2
    ... and you can use `::is_lock_free` to test or even `static_assert` that you're getting a lock-free atomic, if that's a concern. – Tommy Jun 18 '20 at 01:29
  • Problem is that atomics are roughly 10x slower to update than non-atomics and trying to very much optimize a particular snippet of code as much as I possibly can but if it's not guaranteed, then I can't use it. So be it, thanks everyone. – mczarnek Jun 18 '20 at 01:42
  • No. If any thread is writing to a "non-atomic primitive", then the sequencing of all read operations relative to that write is not guaranteed. That is, more or less, the definition of "non-atomic". You therefore need to synchronise (e.g. using a mutex, semaphore, etc) all readers and the writer, to guarantee that reads only occur between writes, not during them. – Peter Jun 18 '20 at 02:18
  • @Peter I'm ok with sequencing not being right, can't guarantee that with atomic either, what I'm not ok with is something like a partial write or garbage being read. – mczarnek Jun 18 '20 at 03:56
  • 1
    @mczarnek - You're misinterpreting the term "sequencing". While sequencing is concerned with order of operations, it is also about whether they are permitted to occur concurrently i.e. whether one is guaranteed to be complete before the other commences. A lack of sequencing guarantees means, among other things, that two operations can occur concurrently (e.g. a read can start before a write is complete - and, if that read and write both affect the same object, the result is what you are describing as "partial write" or "garbage being read"). – Peter Jun 18 '20 at 06:41
  • "Atomic" means that, if thread A performs some operation of the variable, every other thread that looks at the variable will either see the value before the operation was performed, or the value after. It guarantees that no other thread will see any other value. So, what would it mean for a variable to be "non-atomic?" It means, you don't have that guarantee. It means that other threads could see the value before, the value after, or some completely different value that the programmer never intended to be in the variable at all. What could go wrong? – Solomon Slow Jun 18 '20 at 11:25
  • What kind of an atomic variable do you use? If you use an atomic int it shouldn't be 10x slower. If you only care for atomicity, and not visibility or ordering, then always use memory order relaxed. If you don't specify memory order requirements the costly default will be taken which is sequentially consistent. – ciamej Jun 18 '20 at 11:57
  • std::memory_order looks very interesting. When I benchmark it though, roughly the speed, actually slightly slower.. interesting. That being said it's more like 5x slower, not 10x. 0.00174 us to increment int, 0.00874 us to increment atomic int. 0.01 to increment atomic int with memory order relaxed. – mczarnek Jun 18 '20 at 14:32
  • 1
    Now it makes sense. Atomic increment is a very costly operation! Changing memory order won't give you any benefits, because on the hardware level a special machine instruction needs to be executed. Incrementing is actually: reading, adding one, and writing. If you can guarantee there will be only one writer, instead of doing atomic increment, you can do atomic load (relaxed), locally increment, and then atomic store (relaxed). This will be an order of magnitude faster. – ciamej Jun 18 '20 at 18:27
  • Thanks ciamej. It was actually slightly slower(0.0103 us.. as with all of these averaged over 1 billion runs) to load, increment, store. However, given that was only a test and what I actually want to do is far more than increment and all those steps in between will be cheap.. exactly what I want. Memory ordering makes a lot more sense too now. – mczarnek Jun 19 '20 at 01:45
  • 2
    Probably in the non-atomic int test, the value was kept all the time in a cpu register and written to memory only at the end of the loop. That's why it seems so fast. If you don't use any synchronization or atomic variables, the compiler is free to perform such optimizations, which make sense from a single-threaded point of view, but would actually break your code when multiple threads access the same variable. – ciamej Jun 19 '20 at 10:54

1 Answers1

11

No, there are no guarantees whatsoever.

Though I really should stop there because it's a complete answer, if you think "how could this possibly go wrong", consider an implementation where a write to a non-atomic variable isn't atomic. So if you have 0x2F written and then write 0x30, it's possible that another thread might read the first nibble before the write and the second nibble after and get 0x20.

Also, suppose a non-atomic variable has the value zero and this code runs:

#define LAUNCH 1
#define DO_NOT_LAUNCH 0

if (war_has_been_declared)
     non_atomic_variable = LAUNCH;
else
     non_atomic_variable = DO_NOT_LAUNCH;

No rule prohibits the implementation from optimizing the code to this:

non_atomic_variable = LAUNCH;
if (! war_has_been_declared)
     non_atomic_variable = DO_NOT_LAUNCH;

Which means another thread might see a LAUNCH order even if war has not been declared!

But it's important to remember that there simply aren't any guarantees. It doesn't matter whether or not you can think of a plausible way it can go wrong.

David Schwartz
  • 179,497
  • 17
  • 214
  • 278