Does a relaxed memory-order spinlock always break synchronization?

Viewed 111

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?

1 Answers

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.

Related