C ++:释放在构造函数中所需的屏障,该构造函数创建访问构造对象的线程

迈克·斯威尼(Mike Sweeney)

如果我在构造函数中创建一个线程,并且该线程访问该对象,是否需要在该线程访问该对象之前引入释放屏障?具体来说,如果我有下面的代码(wandbox链接),是否需要将互斥锁锁定在构造函数中(注释行)?我需要确保worker_thread_看到写入,run_worker_thread_因此不会立即退出。我意识到在这里使用原子布尔更好,但是我对理解这里的内存顺序含义感兴趣。根据我的理解,我认为我确实需要在构造函数中锁定互斥锁,以确保构造函数中的互斥锁的解锁所提供的释放操作与threadLoop()通过调用互斥锁来实现的获取操作同步。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_;
};

如果我需要类似地将互斥锁锁定在构造函数中,然后再通过一些公共方法从其他线程中读取一堆常规的int(非原子)成员变量,这也使我更加笼统地思考。构造函数中除了读取这些变量的方法外。对于我来说,这似乎与上面的情况略有不同,因为我知道对象将在其他任何线程都可以访问它之前被完全构造,但这似乎不能确保该对象的初始化对于其他线程而言是可见的。构造函数中的释放操作。

ixSci

因为它不需要任何障碍,保证thread与传递给它的函数调用构造函数同步。用Standardese:

构造函数的调用完成与f副本的调用开始同步。


某种形式上的证明:run_worker_thread_ = true;A根据完整表达式的评估顺序对象创建(B之前排序根据上面引用的规则对象构造闭包对象执行(C同步因此,线程间之前发生Çthreadthread

B之前的序列,B与C同步,A发生在C之前->这是标准术语的形式证明。

在分析C ++ 11 +时代的程序时,您应该坚持使用C ++的内存和执行模型,而忽略障碍和重新排序哪些编译器可能会或可能不会做。这些只是实现细节。唯一重要的是C ++术语中的形式证明。编译器必须遵守并做(不做)遵守规则的一切。

但是为了完整起见,让我们用编译器的眼光看一下代码,并尝试理解为什么在这种情况下它不能重新排序任何东西。我们都知道“假设”规则,如果您无法确定某些指令已被重新排序,则编译器可能会对这些指令重新排序。因此,如果我们有一些bool标志设置:

flag1 = true; // A
flag2 = false;// B

允许执行以下行:

flag2 = false;// B
flag1 = true;// A

尽管事实上A在B之前先排序。它可以做到,因为我们无法分辨出差异,我们不能仅仅通过观察程序行为就可以对它重新排序,因为除了“ Sequenceed before”,这些行之间没有关系。但是,让我们回到案例:

run_worker_thread_ = true; // A
worker_thread_ = std::thread(...); // B

看起来这种情况与bool上面的变量相同如果我们不知道thread对象(除了在A表达式之后被排序)是否与某事物同步(为简单起见,我们忽略该事物),那就是这种情况。但是,正如我们发现的那样,如果某个事物在另一个事物之前被排序,而该事物又又与另一个事物同步,那么它就发生在那个事物之前。因此,标准要求在A表达式与B表达式同步之前发生A表达式

这个事实禁止编译器对我们的AB表达式重新排序,因为突然之间我们可以分辨出是否这样做。因为如果这样做,那么C表达式(某物)可能看不到A提供的可见副作用因此,仅通过观察程序执行,我们可能会发现作弊的编译器!因此,它必须使用一些障碍。仅仅是编译器障碍还是硬件障碍都没有关系,它必须存在以保证这些指令不会重新排序。因此,您可能会认为它在构造完成时使用释放围栏,而在关闭对象执行时使用获取围栏。这将大致描述引擎盖下发生的情况。

看起来您也将互斥锁视为一种神奇的事物,它始终有效且不需要任何证据。因此,出于某些原因,您相信mutex而不是thread但事实是它没有魔力,唯一的保证就是lock与先验同步,unlock反之亦然。因此,它提供了相同的质量保证thread提供。

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章