如果我在构造函数中创建一个线程,并且该线程访问该对象,是否需要在该线程访问该对象之前引入释放屏障?具体来说,如果我有下面的代码(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(非原子)成员变量,这也使我更加笼统地思考。构造函数中除了读取这些变量的方法外。对于我来说,这似乎与上面的情况略有不同,因为我知道对象将在其他任何线程都可以访问它之前被完全构造,但这似乎不能确保该对象的初始化对于其他线程而言是可见的。构造函数中的释放操作。
因为它不需要任何障碍,保证了thread
与传递给它的函数调用构造函数同步。用Standardese:
构造函数的调用完成与f副本的调用开始同步。
某种形式上的证明:run_worker_thread_ = true;
(A)根据完整表达式的评估顺序在对象创建(B)之前排序。根据上面引用的规则,对象构造与闭包对象执行(C)同步。因此,甲线程间之前发生Ç。thread
thread
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表达式。
这个事实禁止编译器对我们的A&B表达式重新排序,因为突然之间我们可以分辨出是否这样做。因为如果这样做,那么C表达式(某物)可能看不到A提供的可见副作用。因此,仅通过观察程序执行,我们可能会发现作弊的编译器!因此,它必须使用一些障碍。仅仅是编译器障碍还是硬件障碍都没有关系,它必须存在以保证这些指令不会重新排序。因此,您可能会认为它在构造完成时使用释放围栏,而在关闭对象执行时使用获取围栏。这将大致描述引擎盖下发生的情况。
看起来您也将互斥锁视为一种神奇的事物,它始终有效且不需要任何证据。因此,出于某些原因,您相信mutex
而不是thread
。但事实是它没有魔力,唯一的保证就是lock
与先验同步,unlock
反之亦然。因此,它提供了相同的质量保证是thread
提供。
本文收集自互联网,转载请注明来源。
如有侵权,请联系 [email protected] 删除。
我来说两句