In C++, can atomics suffer spurious stores?
For example, suppose that m
and n
are atomics and that m = 5
initially. In thread 1,
m += 2;
In thread 2,
n = m;
Result: the final value of n
should be either 5 or 7, right? But could it spuriously be 6? Could it spuriously be 4 or 8, or even something else?
In other words, does the C++ memory model forbid thread 1 from behaving as though it did this?
++m;
++m;
Or, more weirdly, as though it did this?
tmp = m;
m = 4;
tmp += 2;
m = tmp;
Reference: H.-J. Boehm & S. V. Adve, 2008, Figure 1. (If you follow the link, then, in the paper's section 1, see the first bulleted item: "The informal specifications provided by ...")
THE QUESTION IN ALTERNATE FORM
One answer (appreciated) shows that the question above can be misunderstood. If helpful, then here is the question in alternate form.
Suppose that the programmer tried to tell thread 1 to skip the operation:
bool a = false;
if (a) m += 2;
Does the C++ memory model forbid thread 1 from behaving, at run time, as though it did this?
m += 2; // speculatively alter m
m -= 2; // oops, should not have altered! reverse the alteration
I ask because Boehm and Adve, earlier linked, seem to explain that a multithreaded execution can
- speculatively alter a variable, but then
- later change the variable back to its original value when the speculative alteration turns out to have been unnecessary.
COMPILABLE SAMPLE CODE
Here is some code you can actually compile, if you wish.
#include <iostream>
#include <atomic>
#include <thread>
// For the orignial question, do_alter = true.
// For the question in alternate form, do_alter = false.
constexpr bool do_alter = true;
void f1(std::atomic_int *const p, const bool do_alter_)
{
if (do_alter_) p->fetch_add(2, std::memory_order_relaxed);
}
void f2(const std::atomic_int *const p, std::atomic_int *const q)
{
q->store(
p->load(std::memory_order_relaxed),
std::memory_order_relaxed
);
}
int main()
{
std::atomic_int m(5);
std::atomic_int n(0);
std::thread t1(f1, &m, do_alter);
std::thread t2(f2, &m, &n);
t2.join();
t1.join();
std::cout << n << "\n";
return 0;
}
This code always prints 5
or 7
when I run it. (In fact, as far as I can tell, it always prints 7
when I run it.) However, I see nothing in the semantics that would prevent it from printing 6
, 4
or 8
.
The excellent Cppreference.com states, "Atomic objects are free of data races," which is nice, but in such a context as this, what does it mean?
Undoubtedly, all this means that I do not understand the semantics very well. Any illumination you can shed on the question would be appreciated.
ANSWERS
@Christophe, @ZalmanStern and @BenVoigt each illuminate the question with skill. Their answers cooperate rather than compete. In my opinion, readers should heed all three answers: @Christophe first; @ZalmanStern second; and @BenVoigt last to sum up.