c++multithreadinglanguage-lawyeratomic

Can the thread that acquires spin-lock later happen first on the timeline?


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?


Solution

  • 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 and C2 denote clock types. t1 and t2 are values returned by C1​::​now() where the call returning t1 happens before ([intro.multithread]) the call returning t2 and both of these calls occur before C1​::​time_point​::​max(). [...]

    [In Table 103] C1​::​is_steady | const bool | true if t1 <= 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.)