Mik*_*ney 4 c++ multithreading memory-barriers c++11
如果我在构造函数中创建一个线程,并且该线程访问该对象,是否需要在该线程访问该对象之前引入释放屏障?具体来说,如果我有下面的代码(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_;
};
Run Code Online (Sandbox Code Playgroud)
如果我需要类似地将互斥锁锁定在构造函数中,然后再通过一些公共方法从其他线程中读取一堆常规的int(非原子)成员变量,那么这也使我更加笼统地思考。构造函数中除了读取这些变量的方法外。对于我来说,这似乎与上面的情况略有不同,因为我知道对象将在其他任何线程都可以访问它之前完全构建,但这似乎无法确保该对象的初始化对于其他线程而言是可见的。构造函数中的释放操作。
因为它不需要任何障碍,保证了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
Run Code Online (Sandbox Code Playgroud)
允许执行以下行:
flag2 = false;// B
flag1 = true;// A
Run Code Online (Sandbox Code Playgroud)
尽管事实上A在B之前排好序。它可以做到,因为我们无法分辨出差异,我们不能仅仅通过观察程序行为来捕获它对指令的重新排序,因为除了“先于顺序”之外,这些行之间没有关系。但是,让我们回到案例:
run_worker_thread_ = true; // A
worker_thread_ = std::thread(...); // B
Run Code Online (Sandbox Code Playgroud)
看起来这种情况与bool上面的变量相同。如果我们不知道thread对象(除了在A表达式之后被排序)是否与某事物同步(为简单起见,我们忽略该事物),那就是这种情况。但是,正如我们发现的那样,如果某个事物在另一事物之前被排序,而该事物又与另一事物同步,那么它就发生在该事物之前。因此,标准要求在A表达式与我们的B表达式同步之前发生。
这个事实禁止编译器对A和B表达式重新排序,因为突然之间我们可以分辨出是否这样做。因为如果这样做,那么C表达式(某物)可能看不到A提供的可见副作用。因此,仅通过观察程序执行,我们可能会发现作弊的编译器!因此,它必须使用一些障碍。仅仅是编译器障碍还是硬件障碍都没有关系,它必须存在以确保这些指令不会重新排序。因此,您可能会认为它在构造完成时使用释放围栏,而在关闭对象执行时使用获取围栏。这将大致描述引擎盖下发生的情况。
看起来您也将互斥锁视为一种神奇的事物,它始终有效并且不需要任何证据。因此,出于某些原因,您相信mutex而不是thread。但事实是它没有魔力,唯一的保证就是lock与先验同步,unlock反之亦然。因此,它提供了相同的质量保证是thread提供。
| 归档时间: |
|
| 查看次数: |
97 次 |
| 最近记录: |