【问题标题】:Safely Destroying a Thread Pool安全销毁线程池
【发布时间】:2015-05-24 13:33:46
【问题描述】:

考虑以下用 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 派生的,通过完整的代码逐步了解其实现背后的目的和一些主观风格的变化。

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

【问题讨论】:

  • 销毁标志是谓词的一部分。条件变量/互斥体/谓词协议的规则#1:永远不要更改,甚至检查,谓词数据,除非受到保护它的互斥体的保护。请记住,互斥锁保护谓词数据;不是条件变量。如果破坏标志可以被原子地修改,它将被豁免。如图所示,不是。显示的代码是正确的,虽然它可能看起来有点矫枉过正,但却是学习的正确模式。
  • 我认为这里的主要困惑是,对于真正的同步内存栅栏或原子读/写操作来说,互斥锁通常是不必要的繁重操作,这两者都可以作为 C 的一部分使用++11 标准...恕我直言,使用互斥锁是在原子之前防止良性数据竞争的“安全”方式,但现在不一定是最佳方式...
  • 我向任何学习 C++ 并发编程的人推荐 Anthony Williams 的书C++ Concurrency In Action,它涵盖了 C++11 并发,包括线程和原子库、C++内存模型等。在 9.1 节中有一个简单线程池的代码。其中等价的变量是std::atomic_bool done,析构函数只是设置done=true,让编译器决定是否需要存储到内存之外的任何东西(它不在 x86 上,因为它的 Total Store Order 内存模型)。见stackoverflow.com/questions/388242/…

标签: c++ multithreading thread-safety threadpool c++14


【解决方案1】:

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

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

【讨论】:

  • 见凯西的回答。
【解决方案2】:

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-02-17
    • 1970-01-01
    • 2020-11-07
    • 1970-01-01
    • 1970-01-01
    • 2013-01-15
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多