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?
Memory orderings specify the way atomic operations synchronize memory. In its weakest Ordering::Relaxed , only the memory directly touched by the operation is synchronized.
The default is std::memory_order_seq_cst which establishes a single total ordering over all atomic operations tagged with this tag: all threads see the same order of such atomic operations and no memory_order_seq_cst atomic operations can be reordered.
C++11 Memory Model. A memory model, a.k.a memory consistency model, is a specification of the allowed behavior of multithreaded programs executing with shared memory [1].
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
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With