安全销毁线程池

Safely Destroying a Thread Pool

考虑以下用 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 分叉出来的,通过一个完整的代码步骤来理解其实现背后的目的和一些主观的风格变化。

我正在向自己介绍并发编程,还有很多东西要学 -- 就目前而言,我是一名新手并发程序员。如果我的问题措辞不正确,请在您提供的答案中进行适当的更正。此外,如果答案可以针对第一次接触并发编程的客户,那将是最好的——对我自己和其他任何新手也是如此。

如果 ThreadPool 对象的拥有线程是唯一原子写入 destroy_ 变量的线程,而工作线程仅从 destroy_ 变量原子读取,则不,不需要互斥锁来保护 ThreadPool 析构函数中的 destroy_ 变量。通常,当必须发生一组不能通过平台上的单个原子指令完成的原子操作时(即,原子交换之外的操作等),互斥锁是必需的。话虽这么说,线程池的作者可能试图在不恢复原子操作(即内存栅栏操作)的情况下对 destroy_ 变量强制执行某种类型的获取语义,and/or 的设置标志本身不被视为原子操作(依赖于平台)...其他一些选项包括将变量声明为 volatile 以防止它被缓存等。您可以查看 this thread 了解更多信息。

如果没有适当的某种同步操作,最坏的情况可能会导致工作程序因 destroy_ 变量缓存在线程上而无法完成。在内存排序模型较弱的平台上,如果您允许存在良性内存竞争条件,那总是有可能的...

C++ 将 数据竞争 定义为多个线程可能同时访问一个对象,其中至少一个访问是写入。具有数据竞争的程序具有未定义的行为。如果你在你的析构函数中写入 destroy 而没有持有互斥量,你的程序将有未定义的行为,我们无法预测会发生什么。

如果您要在其他地方读取 destroy 而不持有互斥锁,则在析构函数写入它时可能会发生读取,这也是一种数据竞争。