Understanding memory_order_relaxed
Asked Answered
R

1

12

I am trying to understand the specifics of memory_order_relaxed. I am referring to this link : CPP Reference.

#include <future>
#include <atomic>

std::atomic<int*> ptr {nullptr};

void fun1(){
        ptr.store(new int{0}, std::memory_order_relaxed);
}

void fun2(){
        while(!ptr.load(std::memory_order_relaxed));
}

int main(){
        std::async(std::launch::async, fun1);
        std::async(std::launch::async, fun2);
}

Question 1: In the code above, is it technically possible for fun2 to be in an infinite loop where it sees the value of ptr as nullptr even if the thread that sets ptr has finished running?

If suppose, I change the code above to something like this instead:

#include <future>
#include <atomic>

std::atomic<int> i {0};
std::atomic<int*> ptr {nullptr};

void fun1(){
        i.store(1, std::memory_order_relaxed);
        i.store(2, std::memory_order_relaxed);
        ptr.store(new int{0}, std::memory_order_release);

}

void fun2(){
        while(!ptr.load(std::memory_order_acquire));
        int x = i.load(std::memory_order_relaxed);
}

int main(){
        std::async(std::launch::async, fun1);
        std::async(std::launch::async, fun2);
}

Related Question: Is it possible in the code above for fun2 to see the value of atomic i as 1 or is it assured that it will see the value 2?

Revisionism answered 1/7, 2015 at 0:40 Comment(7)
Memory ordering has nothing to do with propagation, only with ordering. C++ does in fact not guarantee that changes to memory ever become visible to other threads. What ordering says is that if you observe a memory update, then you can conclude that other effects (those previous to the update) will also be observed.Pesthouse
So in effect it means that the answer to my first question is that it is possible for fun2 to be in an infinite loop (even if i had used some stronger memory ordering like seq_cst) and the answer to my second question is that it is guaranteed that the value of i will be 2 because the previous memory update to ptr is visibleRevisionism
I think there's nothing in the Standard that guarantees that the loop terminates, but of course it will on every real platform. The Standard contains a note that says "please propagate this in reasonable time".Pesthouse
Thanks Kerrek SB. The reason why I had this doubt was because the said link above mentions that when we use memory_order_relaxed, a thread will see some value that it had seen in the past or a later value but never a value that comes before a value already seen in the modification order of the variable. It also mentions that when we use memory_order_relaxed, it is possible to keep seeing the same value again and again and that there is no obligation for it to return values later in the modification order. But according to you this may be the case with a stringent memory_order as well andRevisionism
that this has got nothing to do with the memory order but the standard expects implementations to propagate the changes in a reasonable time for all memory order constraints.Revisionism
I was confused by the same thing and was about to post my own question. My understanding has always been that there is no guarantee that changes will be published to other threads without some kind of synchronization, but some discussions of the C++11 memory model seem to imply otherwise. I understand that, in practice, changes will eventually be published due to how the hardware operates, but this is kind of glossed over in those same sources. Thanks @KerrekSB! Please answer this question so I can upvote it.Oldham
I know this is an old question, but older comments are incorrect and the standard does say that implementations should guarantee these updates are visible in a finite amount of time in section 6.9.2.2 of the C++20 standard, "[intro.progress]".Sixty
L
15

An interesting observation is that, with your code, there is no actual concurrency; i.e. fun1 and fun2 run sequentially, the reason being that, under specific conditions (including calling std::async with the std::launch::async launch policy), the std::future object returned by std::async has its destructor block until the launched function call returns. Since you disregard the return object, its destructor is called before the end of the statement. Had you reversed the two statements in main() (i.e. launch fun2 before fun1), your program would have been caught in an infinite loop since fun1 would never run.

This std::future wait-upon-destruction behavior is somewhat controversial (even within the standards committee) and since I assume you didn't mean that, I will take the liberty to rewrite the 2 statements in main for (both examples) to:

auto tmp1 = std::async(std::launch::async, fun1);
auto tmp2 = std::async(std::launch::async, fun2);

This will defer the actual std::future return object destruction till the end of main so that fun1 and fun2 get to run asynchronously.

is it technically possible for fun2 to be in an infinite loop where it sees the value of ptr as nullptr even if the thread that sets ptr has finished running?

No, this is not possible with std::atomic (on a real platform, as was mentioned in the comments section). With a non-std::atomic variable, the compiler could (theoretically) have chosen to keep the value in register only, but a std::atomic is stored and cache coherency will propagate the value to other threads. Using std::memory_order_relaxed is fine here as long as you don't dereference the pointer.

Is it possible in the code above for fun2 to see the value of atomic i as 1 or is it assured that it will see the value 2?

It is guaranteed to see value 2 in variable x.
fun1 stores two different values to the same variable, but since there is a clear dependency, these are not reordered.

In fun1, the ptr.store with std::memory_order_release prevents the i.store(2) with std::memory_order_relaxed from moving down below its release barrier. In fun2, the ptr.load with std::memory_order_acquire prevents the i.load with std::memory_order_relaxed from moving up across its acquire barrier. This guarantees that x in fun2 will have value 2.

Note that by using std::memory_order_relaxed on all atomics, it would be possible to see x with value 0, 1 or 2, depending on the relative ordering of access to atomic variable i with regards to ptr.store and ptr.load.

Laaspere answered 31/7, 2016 at 0:30 Comment(4)
"Using std::memory_order_relaxed is fine here as long as you don't dereference the pointer.". Is this because the atomic itself has been updated to hold the pointer value, but the memory block allocated by new might not yet be allocated/or might not yet be "released" into the memory ? Thanks a lot.Sparks
@Sparks Yes, that is exactly the reasonLaaspere
std::memory_order_release make sure no reads or writes in the current thread can be reordered after this store, but it cannot make sure the order of i.store(1, std::memory_order_relaxed); and i.store(2, std::memory_order_relaxed);. If these two store operations are reordered, is it possible that the value of i is 1 before ptr.store?Hyades
@Hyades No this is not possible because all threads observe changes to a single variable in the same order (aka modification order). Since the releasing thread writes 1 to i followed by 2, all threads will see that.Laaspere

© 2022 - 2024 — McMap. All rights reserved.