mardi 27 août 2019

C++: Release barriers required in constructor?

If I create a thread in a constructor and if that thread accesses the object do I need to introduce a release barrier before the thread accesses the object? Specifically, if I have the code below (wandbox link) do I need to lock the mutex in the constructor (the commented out line)? I need to make sure that the worker_thread_ sees the write to run_worker_thread_ so that is doesn't immediately exit. I realize using an atomic boolean is better here but I'm interested in understanding the memory ordering implications here. Based on my understanding I think I do need to lock the mutex in the constructor to ensure that the release operation that the unlocking of the mutex in the constructor provides synchronizes with the acquire operation provided by the locking of the mutex in the threadLoop() via the call to shouldRun().

class ThreadLooper {
 public:
   ThreadLooper(std::string thread_name)
       : thread_name_{std::move(thread_name)}, loop_counter_{0} {
        //std::lock_guard<std::mutex> lock(mutex_);
        run_worker_thread_ = true;
        worker_thread_ = std::thread([this]() { threadLoop(); });
        // mutex unlock provides release semantics
   }

   ~ThreadLooper() {
     {
        std::lock_guard<std::mutex> lock(mutex_);
        run_worker_thread_ = false;
     }
     if (worker_thread_.joinable()) {
       worker_thread_.join();
     }
     cout << thread_name_ << ": destroyed and counter is " << loop_counter_
          << std::endl;     
   }

 private:
  bool shouldRun() {
      std::lock_guard<std::mutex> lock(mutex_);
      return run_worker_thread_;
  }

  void threadLoop() {
    cout << thread_name_ << ": threadLoop() started running"
         << std::endl;
    while (shouldRun()) {
      using namespace std::literals::chrono_literals;
      std::this_thread::sleep_for(2s);
      ++loop_counter_;
      cout << thread_name_ << ": counter is " << loop_counter_ << std::endl;
    }
    cout << thread_name_
         << ": exiting threadLoop() because flag is false" << std::endl;
  }

  const std::string thread_name_;
  std::atomic_uint64_t loop_counter_;
  bool run_worker_thread_;
  std::mutex mutex_;
  std::thread worker_thread_;
};

This also got me to thinking about more generally if I were to initialize a bunch of regular int (not atomic) member variables in the constructor that were then read from other threads via some public methods if I would need to similarly lock the mutex in the constructor in addition to in the methods that read these variables. This seems slightly different to me than the case above since I know that the object would be fully constructed before any other thread could access it, but that doesn't seem to ensure that the initialization of the object would be visible to the other threads without a release operation in the constructor.

Aucun commentaire:

Enregistrer un commentaire