Consider this example:
#include <iostream>
#include <atomic>
#include <thread>
struct SpinLock{
std::atomic<bool> state;
void lock(){
bool expected = false;
while(!state.compare_exchange_strong(expected,true,std::memory_order::acquire,std::memory_order::relaxed)){
expected = false;
}
}
void unlock(){
state.store(false,std::memory_order::release);
}
};
int main(){
auto spin_lock = SpinLock{false};
int i = 0;
std::thread t1([&](){
std::this_thread::sleep_for(std::chrono::seconds(1));
spin_lock.lock();
auto time_stamp = std::time(nullptr);
std::cout<<"t1 " <<time_stamp <<" "<< i<<"\n"; // #1
spin_lock.unlock();
});
std::thread t2([&](){
spin_lock.lock();
i = 1;
auto time_stamp = std::time(nullptr);
std::cout<<"t2 " <<time_stamp<<" "<< i<<"\n"; // #2
spin_lock.unlock();
});
t1.join();
t2.join();
}
From the perspective of C++ standard, Is it possible that #1
prints i==0
and a timestamp value representing a later time while #2
prints i==1
and a timestamp value representing an earlier time?
If the value of i
#1
reads is 0
, that is, the store operations to state
in t1
is earlier in the modification order than that of t2(i.e. the CAS operation in t1
wins the race so it firstly acquires the lock), otherwise the read value must be 1
since the lock
in t1
would synchronize with the unlock
in t2. The modification order is irrelevant to the order in the timeline, IIUC, the outcome is
t1 1729172229 0
t2 1729172228 1
this outcome is unintuitive, however, from the perspective of C++ standard, is it a possible outcome?
The C++ standard doesn't forbid this for std::time
. In fact it doesn't say much of anything about how std::time
behaves, but defers to the C standard, which doesn't address this issue; so we have to presume it is allowed.
However, it does forbid it for std::chrono::steady_clock
or any other clock type C
for which C::is_steady == true
. See time.clock.req p2:
In Table 103
C1
andC2
denote clock types.t1
andt2
are values returned byC1::now()
where the call returningt1
happens before ([intro.multithread]) the call returningt2
and both of these calls occur beforeC1::time_point::max()
. [...][In Table 103]
C1::is_steady
|const bool
|true
ift1 <= t2
is always true and the time between clock ticks is constant, otherwise false.
Your spin lock has proper acquire-release ordering, such that unlock synchronizes with lock, and one critical section happens before the other. So if you replaced std::time()
with std::chrono::steady_clock::now()
, then if t1 observes i == 0
, we can conclude that the critical section in t1 happens before the one in t2, and thus the time printed in t1 must be no greater than the time printed in t2. (They could of course be equal.)