4

Consider the following code:

int nonatom = 0;
std::atomic<int> atom{0};

// thread 1
nonatom = 1;
atom.store(1, std::memory_order_release);

// thread 2
while (atom.load(std::memory_order_relaxed)!=1); // spinlock waits for t1
atom.store(2, std::memory_order_relaxed);

// thread 3
if (atom.load(std::memory_oder_acquire)==2) // consider the case that this is true
    int foo = nonatom; // read non-atomic
// Is foo guaranteed to be 1?
// Undefined behavior?

In the case that thread 3 reads the value 2 from atom, is it guaranteed to see the value 1 in nonatom?

Judging from the definition of the happens-before and synchronize-with relations, I would say that it cannot be said that the write to nonatom happens-before the read, because the t3's acquire does not sync with the release in thread 1, because it does not read from the release-sequence but instead reads the value from a store of another thread, thread 2. In this case there would be a data-race between thread 1 and 3 because the operations compete for the same non-atomic and one does not happen-before the other.

However, it is commonly informally said that a release guarantees that writes cannot be reordered after it while an acquire guarantees that reads cannot be reordered before it, which would make it seemingly logically impossible for nonatom to be read while or before it is written to.

My analysis of this is that by the standard alone, the code is incorrect, but could it actually break on any realistic implementation, given how release and acquire are usually implemented in machine code? What is your assessment of this example?

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
JMC
  • 1,723
  • 1
  • 11
  • 20
  • **C++ Concurrency In Action** by **Anthony Williams** is a good book to buy if you are serious about multithreading. – Phil1970 Jan 01 '21 at 18:46
  • Thanks for the recommendation. However, I'm not sure if it would answer this question as it's more of a question of whether a not strictly compliant code might still be logically guaranteed to work based on known implementations. – JMC Jan 01 '21 at 23:00
  • 2
    [this question](https://stackoverflow.com/questions/45694459/using-an-atomic-read-modify-write-operation-in-a-release-sequence) is somewhat related – LWimsey Jan 02 '21 at 00:08
  • 1
    The book has a few examples but none seems to correspond to yours. I would think that by transitivity, foo will always be 1. On the order hand, if I would write the code, I might go with the safer approch (release/acquire in thread 2). **Link in LWimsey comment might be more useful.** – Phil1970 Jan 02 '21 at 00:25

1 Answers1

0

The atom.store(2, std::memory_order_relaxed); in the example breaks the release sequence headed by atom.store(1, std::memory_order_release);. Even if memory_order_release were used in place of memory_order_relaxed it would still break it. And because of the failed release-acquire there is a data race on nonatom.

But atomic read-modify-write operations don’t break release sequence, thus replacing atom.store(2, std::memory_order_relaxed); with atom.fetch_add(1, std::memory_order_relaxed); or atom.exchange(2, std::memory_order_relaxed); would fix the race and foo would be guaranteed to be 1.

dened
  • 4,253
  • 18
  • 34
  • The question already concluded this, that ISO C++ doesn't formally guarantee it because there isn't a release-sequence. The question is whether any real implementation could actually break this code. (Some people would consider that to not be a useful question, but I think it's interesting; real world CPUs don't make stores visible to other cores until all earlier speculation has been verified, including load results and branch-prediction.) – Peter Cordes Feb 06 '22 at 00:50
  • @PeterCordes Yes, but it still might be useful for someone to learn that simply replacing `store` with `exchange` can fix the race. – dened Feb 06 '22 at 07:21