1

It has been mentioned by several, for example here c++ what happens when in one thread write and in second read the same object? (is it safe?) that if two threads are operating on the same variable without atomics and lock, reading the variable can return neither the old value nor the new value.

I don't understand why this can happen and I cannot find an example such things happen, I think load and store is always one single instruction which will not be interrupted, then why can this happen?

1a1a11a
  • 1,187
  • 2
  • 16
  • 25
  • If you're modifying values that aren't atomic or gated by a mutex then no guarantees about what you read can be made. "I think load and store is always one single instruction" is where you're wrong. That's two operations. Also if you write to something it could be cached, so another CPU core might be using stale values until the cache is flushed or refreshed. – tadman Jul 03 '20 at 22:47
  • 2
    @tadman: I do not think they mean there is a one-instruction load-and-store. They meant load is one instruction and store is one instruction. (Which may be wrong, and that is a reason for which non-atomic loads and stores are not guaranteed, but it is not what your comment speaks to.) Stale values in the cache are also not relevant; atomic accesses can also involve stale values. Staleness is an ordering issue, not an atomicity issue. – Eric Postpischil Jul 03 '20 at 23:32

3 Answers3

2

From a language-lawyer point of view (i.e. in terms of what the C or C++ spec says, without considering any particular hardware the program might be running on), operations are either defined or undefined, and if operations are undefined, then the program is allowed to do literally anything it wants to, because they don't want to constrain the performance of the language by forcing compiler writers to support any particular behavior for operations that the programmer should never allow to happen anyway.

From a practical standpoint, the most likely scenario (on common hardware) where you'd read a value that is neither-old-nor-new would be the "word-tearing" scenario; where (broadly speaking) the other thread has written to part of the variable at the instant your thread reads from it, but not to the other part, so you get half of the old value and half of the new value.

Jeremy Friesner
  • 70,199
  • 15
  • 131
  • 234
2

It has been mentioned by several, for example here c++ what happens when in one thread write and in second read the same object? (is it safe?) that if two threads are operating on the same variable without atomics and lock, reading the variable can return neither the old value nor the new value.

Correct. Undefined behavior is undefined.

I don't understand why this can happen and I cannot find an example such things happen, I think load and store is always one single instruction which will not be interrupted, then why can this happen?

Because undefined behavior is undefined. There is no requirement that you be able to think of any way it can go wrong. Do not ever think that because you can't think of some way something can break, that means it can't break.

For example, say there's a function that has an unsynchronized read in it. The compiler could conclude that therefore this function can never be called. If it's the only function that could modify a variable, then the compiler could omit reads to that variable. For example:

int j = 12;

// This is the only code that modifies j
int q = some_variable_another_thread_is_writing;
j = 0;

// other code
if (j != 12) important_function();

Since the only code that modifies j reads a variable another thread is writing, the compiler is free to assume that code will never execute, thus j will always be 12, and thus the test of j and the call to important_function can be optimized out. Ouch.

Here's another example:

if (some_function()) j = 0;
    else j = 1;

If the implementation thinks that some_function will almost always return true and can prove some_function cannot access j, it is perfectly legal for it to optimize this to:

j = 0;
if (!some_function()) j++;

This will cause your code to break horribly if other threads mess with j without a lock or j is not a type defined to be atomic.

And do not ever think that some compiler optimization, though legal, will never happen. That has burned people over and over again as compilers get smarter.

David Schwartz
  • 179,497
  • 17
  • 214
  • 278
  • The C standard does not exist in a vacuum; the committee would not have defined the language this way if there were not a reason: There would be value in being able to read and write in multiple threads without locks, so the committee would not have denied users this value unless there were some justification for that. It is a reasonable question to ask what that justification is. This answer does not speak to that question at all. – Eric Postpischil Jul 03 '20 at 23:33
  • Both this answer and Eric's answers are reasonable and useful, thank you! – 1a1a11a Jul 04 '20 at 00:41
  • @EricPostpischil I agree. I answered the question actually asked. It's very dangerous to answer this question with that kind of answer because it suggests that if you can't think of a raitonale (or think the rationale you can think of doesn't apply) you then have a guarantee that you don't have. Someone reading your answer might think, "well, I'm never going to use 16-bit platforms, so I'm safe". – David Schwartz Jul 04 '20 at 01:54
  • @EricPostpischil Also, there are a huge number of real-world optimizations that would not be possible if platforms had to make this work. That alone would be good enough reason to leave it as undefined behavior. (For example, making a write to memory even where the unoptimized code would not do so.) – David Schwartz Jul 04 '20 at 03:58
  • Eric, Jeremy and David's answers are all very good, there is no single correct/best answer, thank you, all! – 1a1a11a Jul 04 '20 at 05:17
2

For on example, C may be implemented on hardware which supports only 16-bit accesses to memory. In this case, loading or storing a 32-bit integer requires two load or store instructions. A thread performing these two instructions may be interrupted between their executions, and another thread may execute before the first thread is resumed. If that other thread loads, it may load one new part and one old part. If it stores, it may store both parts, and the first thread, when resumed, will see one old part and one new part. And other such mixes are possible.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
  • Thank you, @Eric! This is very clear. May I ask is this the only condition? Assume my software only works on modern x86-64 architecture and my usage can tolerate stale value, is it safe not using atomics and locking? – 1a1a11a Jul 04 '20 at 00:38
  • @1a1a11a: No, it is not the only condition. First, be aware that the fact that the target hardware has atomic load/store instructions does not mean the C implementation uses them. You cannot assume “The hardware has this feature, therefore my C implementation does too.” Maybe there is some weird situation where the compiler thinks it can optimize some access to object `x` while it is doing something else with object `y`, and it uses some weird load sequence. Or maybe the compiler decided to save space and split `x` across a word boundary to avoid padding, but then it needs two loads for it. – Eric Postpischil Jul 04 '20 at 00:45
  • @1a1a11a: These are not necessarily likely things, but the point is that proper engineering derives conclusions from specifications: If it is specified, you can write code that you know will behave a certain way. If it is not specified, you cannot be sure. (Unfortunately, many things are not properly documented, and people have to proceed without high-quality assurances, but such is life.) – Eric Postpischil Jul 04 '20 at 00:47
  • I see, the answers/discussions here are very helpful. I learned a lot. Thank you! – 1a1a11a Jul 04 '20 at 05:16