【问题标题】:Why Threadpool not speed up but slow a lot?为什么 Threadpool 速度不快反而慢了很多?
【发布时间】:2021-08-12 00:56:15
【问题描述】:

我在 github 上阅读了一些线程池的代码。

这是一个很好的,我修改过的。

#pragma once
#include <vector>
#include <queue>
#include <memory>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <future>
#include <functional>
#include <stdexcept>

class ThreadPool {
public:
    ThreadPool(size_t);
    template<class F, class... Args>
    auto enqueue(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type>;  // -> apparently define return type is future
    ~ThreadPool();
private:
    // need to keep track of threads so we can join them
    std::vector< std::thread > workers;
    // the task queue
    std::queue< std::function<void()> > tasks;
    
    // synchronization
    std::mutex queue_mutex;
    std::condition_variable condition;
    bool stop;
};
 
// the constructor just launches some amount of workers
inline ThreadPool::ThreadPool(size_t threads) : stop(false) {
  for(size_t i = 0;i<threads;++i)
    workers.emplace_back(  // thread variable, emplace back just pass in a function for thread to construct
      [this] {
        for(;;) {
          std::function<void()> task;
          {   
            std::unique_lock<std::mutex> lock(this->queue_mutex);
            this->condition.wait(lock, [this] { return this->stop || !this->tasks.empty(); }); 
            // wait stop true or task not empty
            if(this->stop && this->tasks.empty()) return;  // only on stop and empty task, perfect exit
            // if only stop, still will hanle left task
            task = std::move(this->tasks.front());  // get the first task
            this->tasks.pop();
          }   
          task();
        }   
      }); 
}

// add new work item to the pool
template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type> {
  using return_type = typename std::result_of<F(Args...)>::type;

  auto task = std::make_shared< std::packaged_task<return_type()> >(std::bind(std::forward<F>(f), std::forward<Args>(args)...));     
  std::future<return_type> res = task->get_future();
  {
    std::unique_lock<std::mutex> lock(queue_mutex);
    // don't allow enqueueing after stopping the pool
    if(stop) throw std::runtime_error("enqueue on stopped ThreadPool");
    tasks.emplace([task](){ (*task)(); });
  }
  condition.notify_one();
  return res;
}

// the destructor joins all threads
inline ThreadPool::~ThreadPool() {
  {
    std::unique_lock<std::mutex> lock(queue_mutex);
    stop = true;
  }
  condition.notify_all();
  for(std::thread &worker: workers) worker.join();
}

我用它来加速我的代码。例如,我有 1000 个独立任务,每个任务花费大约 1 秒。

我发现使用这个线程池,它比一个循环慢。

所以,我写了一些小代码来检查。

这是我的代码。

#include "./time_util.h" // this is a timer class, just use to get time
#include "./thread_pool.hpp"  //  this is the class Threadpool header

size_t i = 0;
void test() {
  // printf("hah\n");
  i++;
}

#define N 1000000
void fun1() {
  printf("func1\n");
  ThreadPool pool(10);
  for (int i = 0; i < N; ++i) {
    pool.enqueue(test);
  }
}

void fun2() {
  printf("func2\n");
  for (int i = 0; i < N; ++i) {
    test();
  }
}

int main(int argc, char** argv) {
  util::time_util::Timer t;
  t.StartTimer();
  if (argc == 1) {
    fun1();
  } else {
    fun2();
  }
  printf("i=%d\n", i); 
  t.EndTimer("1");
}

循环版本花费 0.005 秒 线程池花费 5s。

我的机器cpu是2.2G 24core(48thread),centos7。

而且,线程池的结果不正确(没关系,因为我没有加锁)

你能解释一下吗?这是否意味着对于小任务,我不能使用线程池来加速?

【问题讨论】:

  • 因为每个线程中完成的工作量非常小,而调度的开销要大得多。在 threadproc 中做一些实际的工作。 i++; 也是 UB,因此使所有测试无效。
  • 如果我的独立任务每个只有 1 秒,这是否意味着我无法使用线程池加速? (假设我不能将 10task 绑定为一个)@RichardCritten
  • 与操作系统必须进行上下文切换的所有线程一样,每个线程的工作量需要比上下文切换时间加上线程池本身的开销大 1 个或多个数量级。线程池的理想用例是磁盘或网络 IO。
  • M 的经验表明,为了充分发挥线程池的优势,在线程池中要做的最小单元工作实际上是相当高的。以解压 512x512 JPEG 图像的时间顺序为例。
  • 如果我想加快我的小任务组,有什么好的方法吗? (500 个任务,循环运行成本 80 秒。)@RichardCritten

标签: c++ multithreading stl threadpool


【解决方案1】:

线程池是一个非常“昂贵”的实用程序。如果您有多个繁重的计算任务,这是有道理的,因此线程池引入的开销可以忽略不计。

在您的情况下,i++ 的计算非常简单,您基本上测量线程池的开销。尝试将其替换为需要更长时间的计算,例如 sleep(1),以查看差异。

附带说明:size_t i;i++ 不是线程安全的,您可能会得到错误的结果。请改用atomic&lt;size_t&gt; i;


请注意:线程池对于某些任务很有用,例如在网络服务器中处理工作线程,但对于其他任务(例如实现排序算法或对数字求和)则不然。如果你想并行计算,C++17 为许多标准算法带来了并行执行策略,所有线程处理都为你处理好了。见这里https://en.cppreference.com/w/cpp/algorithm和这里Parallel Loops in C++

最后,永远不要低估现代 CPU 上单核所能产生的巨大能量。

【讨论】:

  • 我正在加速我的代码,它运行 500 个相关任务。如果我循环运行,它花费大约 80 秒,但是当我使用线程池时,它总是大约 100 秒,这让我很困惑。如果我想加快此类任务的速度,您能提供一些建议吗?
  • 如果你使用 500 个任务,并且每个任务都在没有线程池的情况下执行 sleep(1),那么它必须花费 500s(而不是 80s)。
猜你喜欢
  • 2015-02-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-03-21
  • 2020-04-13
  • 1970-01-01
  • 2013-10-05
  • 2011-04-26
相关资源
最近更新 更多