4

Following code writes into atomic A, B variable and reads them in opposite order in another thread:

#include <atomic>
#include <thread>

// Initially.
std::atomic<int> A = std::atomic<int>(0);
std::atomic<int> B = std::atomic<int>(0);

// Thread 1
void write()
{
    A.store(1, std::memory_order_release); // line 1
    B.store(1, std::memory_order_release); // line 2
}

// Thread 2
void read()
{
    int valB = B.load(std::memory_order_acquire); // line 3
    int valA = A.load(std::memory_order_acquire); // line 4
}

int main()
{
    std::thread t1(write), t2(read);
    t1.join();
    t2.join();
    return 0;
}

Ideally, given the release-acquire ordering on atomic B, the load on line 3, should synchronize for completion of store on line 1, since it happens to be before release operation on line 2. But the reference only mentions (emphasis, mine) -

All memory writes (non-atomic and relaxed atomic) that happened-before the atomic store from the point of view of thread A, become visible side-effects in thread B.

So, a release-acquire ordering on B, doesn't take into effect the atomic oparations on other variables that aren't relaxed memory ordering?

Is it possible to have then, values for valB = 1, valA = 0, after read() concludes in Thread 2, since B's memory ordering doesn't care about other atomic operations that aren't relaxed? Do we need sequential consistency here?

6
  • Fixed. It was to explain the logic via pseudo-code. I don't think a minimal reproducible example would have helped here.. Commented Feb 6, 2023 at 11:41
  • Great! It's always better to use a real compiling example written in the language you are asking about Commented Feb 6, 2023 at 11:43
  • Not to overly nitpick the example, but.. it's still fundamentally flawed – thread creation is itself a synchronization point. If you're asking about 'thread 1' and 'thread 2' then just make 2 threads; calling write() on the main thread isn't really shorter and definitely changes the nature of the question (unintentionally). Commented Feb 6, 2023 at 11:53
  • @ildjarn what exactly do you mean by thread creation is a synchronization point? Can you pls elaborate, in the context of current example. Thanks Commented Feb 6, 2023 at 12:50
  • @Akash : I'd rather not, which is why I wrote a comment rather than an answer. Please refer to docs for std::thread's constructor, which state that completion of thread's constructor synchronizes-with the beginning of invocation of the supplied callable on the new thread. So in the code shown here, it is flat out impossible for the thread running read to see anything other than 1 and 1, but not for the reasons you're asking about. Commented Feb 6, 2023 at 12:57

1 Answer 1

4

Is it possible to have then, values for valB = 1, valA = 0, after read() concludes in Thread 2, since B's memory ordering doesn't care about other atomic operations that aren't relaxed?

No that's not possible. As you quoted:

All memory writes (non-atomic and relaxed atomic) that happened-before the atomic store from the point of view of thread A, become visible side-effects in thread B.

So when you read 1 from B, the happened-before write to A is visible in the reading thread and thus the following read from A must also return 1.

The part in parentheses that you highlighted might be better worded as "(including non-atomic and relaxed atomic)" – is that what tripped you up?

What does the standard say?

The relevant part of the standard would be atomics.order.1.4:

An atomic operation A that performs a release operation on an atomic object M synchronizes with an atomic operation B that performs an acquire operation on M

The meaning of "synchronizes with" is described in intro.multithread:

An evaluation A inter-thread happens before an evaluation B if A synchronizes with B [...]

An evaluation A happens before an evaluation B (or, equivalently, B happens after A) if

  • A is sequenced before B, or
  • A inter-thread happens before B.

Happens-before is transitive: A.store() happens-before (sequenced before) B.store(), happens-before (synchronizes-with) B.load(), happens-before (sequenced before) A.load(). Thus A.store() happens-before A.load().

The standard explicitly states about atomic objects:

If a side effect X on an atomic object M happens before a value computation B of M, then the evaluation B shall take its value from X or from a side effect Y that follows X in the modification order of M.

So, since A.store(1) happens-before A.load() and there are no other candidate operations Y, A.load() must return 1.

I can also recommend this blog post.

Sign up to request clarification or add additional context in comments.

4 Comments

Yeah, the reference only mentions for non-atomic, and, relaxed atomic operations, am I reading something wrong, then?
I guess the author assumed that it was obvious that this holds for atomic operations and didn't need to feel the need to list them. Note that the actual sentence still says all. Let me see if I can find a more authoritative source for you.
Expanded my answer with the relevant quotes from the standard that clearly specify this.
@Akash: The text on cppreference is/was wrong and doesn't say what the author probably meant (as Chronial says, they probably meant "including", but that's not what they wrote). If B.store was relaxed, an earlier release store could reorder with it, so this is actually an important distinction. (Even an earlier seq_cst store would be allowed to reorder, and could when compiling for AArch64.) (The C++ memory model is formally defined in terms of creating happen-before relationships, not in terms of local reordering of accesses to coherent shared cache like we normally think of.)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.