Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can atomics suffer spurious stores?

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.

like image 358
thb Avatar asked Nov 05 '17 13:11

thb


People also ask

How do atomic variables work?

An atomic variable can be one of the alternatives in such a scenario. Java provides atomic classes such as AtomicInteger, AtomicLong, AtomicBoolean and AtomicReference. Objects of these classes represent the atomic variable of int, long, boolean, and object reference respectively.

What is atomic variable in C?

Atomics as part of the C language are an optional feature that is available since C11. Their purpose is to ensure race-free access to variables that are shared between different threads. Without atomic qualification, the state of a shared variable would be undefined if two threads access it concurrently.


1 Answers

Your code makes use of fetch_add() on the atomic, which gives the following guarantee:

Atomically replaces the current value with the result of arithmetic addition of the value and arg. The operation is read-modify-write operation. Memory is affected according to the value of order.

The semantics are crystal clear: before the operation it's m, after the operation it's m+2, and no thread accesses to what's between these two states because the operation is atomic.


Edit: additional elements regarding your alternate question

Whatever Boehm and Adve may say, the C++ compilers obey to the following standard clause:

1.9/5: A conforming implementation executing a well-formed program shall produce the same observable behavior as one of the possible executions of the corresponding instance of the abstract machine with the same program and the same input.

If a C++ compiler would generate code that could allow speculative updates to interfere with the observable behavior of the program (aka getting something else than 5 or 7), it would not be standard compliant, because it would fail to ensure the guarantee mentioned in my initial answer.

like image 177
Christophe Avatar answered Sep 29 '22 06:09

Christophe