0

Is the following guaranteed to print 1 followed by 2?

auto&& atomic = std::atomic<int>{0};
std::atomic<int>* pointer = nullptr;

// thread 1
auto&& value = std::atomic<int>{1};
pointer = &value;
atomic.store(1, std::memory_order_relaxed);
while (atomic.load(std::memory_order_relaxed) != 2) {}
cout << value.load(std::memory_order_relaxed) << endl;

// thread 2
while (atomic.load(std::memory_order_relaxed) != 1) {}
cout << pointer->load(std::memory_order_relaxed); << endl;
pointer->fetch_add(1, std::memory_order_relaxed);
atomic.store(2, std::memory_order_relaxed) {}

If not, what are the possible outputs here? What does the standard say about initialization and memory orders for this case?

Curious
  • 19,352
  • 6
  • 45
  • 114
  • 1
    The first thing that comes to mind is undefined behavior – curiousguy12 Jul 22 '18 at 05:29
  • @curiousguy12 Could you explain? It seems like the atomic variable should be properly initialized before its address can be taken? – Curious Jul 22 '18 at 05:35
  • Sorry, did not include explanation.. `pointer` in th1 is assigned an address followed by `atomic.store(1, relaxed)`. Th2 picks up that store, but based on relaxed ordering, there is no synchronization; i.e., th2 may dereference `pointer` while it is still `nullptr` – curiousguy12 Jul 22 '18 at 05:39
  • 1
    In your comment, the core issue is in the word "before". Without synchronization, there is no before or after. Access to `pointer` is simply unordered – curiousguy12 Jul 22 '18 at 05:48
  • That makes sense to me, do you want to turn that into an answer? – Curious Jul 22 '18 at 05:49
  • @Curious You have a **data race**, that's UB for you. No amount of `std::atomic` use will cancel that. – curiousguy Jul 22 '18 at 06:10
  • One of the best explanations [C++11 introduced a standardized memory model](https://stackoverflow.com/questions/6319146/c11-introduced-a-standardized-memory-model-what-does-it-mean-and-how-is-it-g) – badola Jul 22 '18 at 06:56

1 Answers1

4

As mentioned in a comment, the use of 'relaxed' ordering prevents any necessary inter-thread synchronization from happening and so, access to pointer is unsynchroinzed (or unordered).
That means thread 2 may dereference pointer while it still has the value nullptr.
Also, since pointer is a non-atomic type (i.e. a regular pointer), it may not be accessed this way between threads. Technically you have a data race, and that leads to undefined behavior.

A solution is to strengthen memory ordering a bit. I think using acquire/release ordering on atomic should be sufficient:

auto&& atomic = std::atomic<int>{0};
std::atomic<int>* pointer = nullptr;

// thread 1
auto&& value = std::atomic<int>{1};
pointer = &value;
atomic.store(1, std::memory_order_release);
while (atomic.load(std::memory_order_acquire) != 2) {}
cout << value.load(std::memory_order_relaxed) << endl;

// thread 2
while (atomic.load(std::memory_order_acquire) != 1) {}
cout << pointer->load(std::memory_order_relaxed); << endl;
pointer->fetch_add(1, std::memory_order_relaxed);
atomic.store(2, std::memory_order_release) {}

With this ordering in place, the outcome is guaranteed to print

1
2
curiousguy12
  • 1,241
  • 6
  • 12