考虑以下用C ++ 14编写的普通线程池的实现。
观察每个线程正在睡眠,直到它被通知唤醒 - 或者一些虚假的唤醒呼叫 - 并且以下谓词评估为true
:
std::unique_lock<mutex> lock(this->instance_mutex_);
this->cond_handle_task_.wait(lock, [this] {
return (this->destroy_ || !this->tasks_.empty());
});
此外,观察ThreadPool
对象使用数据成员destroy_
来确定它是否被销毁 - 析构函数已被调用。将此数据成员切换到true
将通知每个工作线程它完成当前任务的时间,然后任何其他排队任务与销毁此对象的线程同步;除了禁止enqueue
成员函数。
为方便起见,析构函数的实现如下:
ThreadPool::~ThreadPool() {
{
std::lock_guard<mutex> lock(this->instance_mutex_); // this line.
this->destroy_ = true;
}
this->cond_handle_task_.notify_all();
for (auto &worker : this->workers_) {
worker.join();
}
}
问:我不明白为什么在析构函数中切换destroy_
到true
时锁定对象的互斥锁是必要的。此外,是否只需要设置其值或是否也需要访问其值?
BQ:此线程池实现是否可以在保持其原始目的的同时得到改进或优化;一个线程池,它可以汇集N
个线程并将任务分配给它们以便同时执行?
这个线程池实现是从Jakob Progsch's C++11 thread pool repository分叉完成的,通过一个完整的代码步骤来理解其实现背后的目的和一些主观风格的变化。
我自我介绍并发编程,还有很多东西需要学习 - 我现在是一名新手并发程序员。如果我的问题措辞不正确,请在您提供的答案中进行适当的更正。此外,如果答案可以面向首次引入并发编程的客户,那么这对我自己和任何其他新手来说都是最好的。
答案 0 :(得分:1)
如果ThreadPool
对象的拥有线程是唯一以原子方式写入destroy_
变量的线程,并且工作线程只是从destroy_
变量原子读取,那么不,不需要互斥锁来保护destroy_
析构函数中的ThreadPool
变量。通常,当必须通过平台上的单个原子指令(即,超出原子交换的操作等)完成一组原子操作时,互斥是必要的。话虽这么说,线程池的作者可能试图在destroy_
变量上强制某种类型的获取语义而不恢复原子操作(即内存栅栏操作),和/或标志本身的设置不被视为原子操作(取决于平台)...其他一些选项包括将变量声明为volatile
以防止其被缓存等。您可以查看this thread以获取更多信息。
如果没有某种同步操作,最糟糕的情况可能会导致由于destroy_
变量缓存在线程上而无法完成的工作人员。在内存排序模型较弱的平台上,如果你允许存在良性记忆竞争条件,这种情况总是存在......
答案 1 :(得分:1)
C ++将数据竞争定义为可能同时访问对象的多个线程,其中至少一个访问是写入。具有数据争用的程序具有未定义的行为。如果您在没有持有互斥锁的情况下在析构函数中写入destroy
,那么您的程序将具有未定义的行为,我们无法预测会发生什么。
如果你在没有持有互斥锁的情况下在其他地方阅读destroy
,那么当析构函数写入它时也可能发生这种读取,这也是数据竞争。