Within one thread, steady_clock::now()
is guaranteed to return monotonically increasing values. How does this interact with memory ordering and reads observed by multiple threads?
atomic<int> arg{0};
steady_clock::time_point a, b, c, d;
int e;
thread t1([&](){
a = steady_clock::now();
arg.store(1, memory_order_release);
b = steady_clock::now();
});
thread t2([&](){
c = steady_clock::now();
e = arg.load(memory_order_acquire);
d = steady_clock::now();
});
t1.join();
t2.join()
assert(a <= b);
assert(c <= d);
Here's the important bit:
if (e) {
assert(a <= d);
} else {
assert(c <= b);
}
Can these assert ever fail? Or have I misunderstood something about acquire release memory order?
What follows is mostly an explanation and elaboration of my code example.
Thread t1
writes to the atomic arg
. It also records the current time before and after the write in a
and b
respectively. steady_clock
guarantees that a <= b
.
Thread t2
reads from the atomic arg
and saves the value read in e
. It also records the current time before and after the read in c
and d
respectively. steady_clock
guarantees that c <= d
.
Both threads are then joined. At this point e
could be 0
or 1
.
If e
is 0
, then t2
read the value before t1
wrote it. Does this also imply that c = now()
in t2
happened before b = now()
in t1
?
If e
is 1
then t1
wrote the value before t2
read it. Does this also imply that a = now()
in t1
happened before d = now()
in t2
?
Here are some existing questions that don't answer what I'm asking:
Is there any std::chrono thread safety guarantee even with multicore context?
I'm not asking whether now()
is thread-safe. I know it is.
Is steady_clock monotonic across threads?
This one is much closer, but that example uses mutex
. Can I make the same assumptions about memory orderings weaker than seq_cst
?
The question is unfortunately incomplete. Acquire and release memory orders prevent reordering in one direction only. Consider
t1
:a
indeed gets assigned beforearg.store
. But the compiler is free to reorder the assignment tob
to happen before as well.Similarly, in
t2
:d
indeed gets assigned afterarg.load
. But the compiler is free to reorder the assignment toc
to happen after as well.Therefore
assert(c <= b)
may fail.There is however one caveat. This is only true if
steady_clock::now()
is known to have no observable effects. However, on platforms where I have looked at its implementation, this results in a call to some opaque library function. Compilers must assume that such calls could have observable effects (such as modifying a volatile variable or terminating the program), and therefore will not reorder the calls.The question would be valid if the important asserts consisted solely of
or if both memory orders were
acq_rel
orseq_cst
.So, given that either the memory orders are corrected or the assert is checked only when
e != 0
, I can be sure that the relevant calls tosteady_clock::now()
actually happen in the correct order, even across threads.But does this also imply that the values returned are consistent with that order across both
t1
andt2
, rather than just withint1
andt2
in isolation?I don't know, which is why I won't accept this answer.