c++线程池

327 阅读4分钟

什么是线程池?

当我们遇到耗费时间的任务却又不想主线程阻塞在这里,就可以考虑使用多线程提高效率。c++11的thread为多线程提供了支持。线程在std::thread对象创建时启动,需要传入一个函数名。这个函数在其所属线程上运行,直到函数执行完毕,线程结束。

启动线程时,需要明确指定是要等待线程结束,还是让其自主运行。如果std::thread对象销毁前还没有做出决定,线程就会终止(std:thread的析构函数会调用std::terminate())

  • 加入式

     void hello() {
         cout << "hello, world" << endl;
     }
     ​
     int main() {
         thread t(hello);
         t.join();
         cout << "me, too" << endl;
         return 0;
     }
     //输出结果唯一;
     //hello, world
     //me, too
    
  • 分离式

     void hello() {
         cout << "hello, world" << endl;
     }
     ​
     int main() {
         thread t(hello);
         t.detach();
         cout << "me, too" << endl;
         return 0;
     }
     //以下结果都有可能;
     //1
     //me, toohello, world
     //2
     //me, too
     //hello, world
    

    如果不等待线程,就必须保证线程结束前,可访问的数据的有效性。在使用多线程的时候,尽量使线程函数的功能齐全,将数据复制到线程中,而非复制到共享数据中。

但如果遇到一个任务我们就创建一个线程,就会浪费大量的时间在线程的创建和销毁上,毕竟创建线程是需要系统调用进入内核态的。所以我们在一开始就批量创建线程,也就是线程池,通过复用线程来节省开销。一个线程池大致结构如下: image.png 主线程和N个工作线程类似单生产者/多消费者模型,它们通过一个任务队列来交互。主线程调用addTask往队列中添加任务,N个工作线程从队列中取出任务并执行。

有哪些问题?

基本实现如下:

#include <mutex>
#include <vector>
#include <queue>
#include <condition_variable>
#include <thread>
#include <functional>

class threadPool_v1 {
 public:
  threadPool_v1(int threads_num):stop(false), threads(std::vector<std::thread>(threads_num)){

  }

  ~threadPool_v1() = default;

  ///拷贝/移动构造函数,拷贝/移动赋值运算符均设置为delete
  threadPool_v1(const threadPool_v1&) = delete;
  threadPool_v1(threadPool_v1&&) = delete;

  threadPool_v1& operator=(const threadPool_v1&) = delete;
  threadPool_v1& operator=(threadPool_v1&&) = delete;

  /// 开启多个线程
  void init() {
    for(int i = 0; i < threads.size(); i++) {
      threads[i] = std::thread(std::bind(&threadPool_v1::run, this));
    }
  }

  void shutdown() {
    stop = true;
    //必须加上,不然线程阻塞在空队列上
    cond.notify_all();
    for(int i = 0; i < threads.size(); i++) {
      if(threads[i].joinable()) {
        threads[i].join();
      }
    }
  }

  bool addTask(std::function<void()> fun) {
    std::unique_lock<std::mutex> lock(mtx);
    if(stop) {
      return false;
    }
    tasks.push(fun);
    cond.notify_one();
    return true;
  }

  void run() {
    while(!stop) {
      std::unique_lock<std::mutex> lock(mtx);
      if(tasks.empty()) {
        cond.wait(lock);
      }
      auto task = std::move(tasks.front());
      tasks.pop();
      lock.unlock();
      if(task) {
        task();
      }
    }
  }


 private:
  std::mutex mtx;
  std::condition_variable cond;
  bool stop;
  std::queue<std::function<void()>> tasks;
  std::vector<std::thread> threads;
};

#endif//THREADPOOL__THREADPOOL_V1_H_

在main.cpp中测试代码如下:

void printNum() {
  int num = random();
  std::cout << num << std::endl;
}


int main() {
  threadPool_v1 pool(2);
  std::function<void()> func = printNum;
  pool.init();
  pool.addTask(printNum);
  pool.addTask(printNum);
  pool.addTask(printNum);
  //等待任务执行完成
  std::this_thread::sleep_for(std::chrono::seconds(2));//sleep 2秒
  pool.shutdown();
  std::cout << "shutdown" << std::endl;
  std::this_thread::sleep_for(std::chrono::seconds(2));//sleep 2秒
}

那么我的实现有哪些需要注意的问题或者说改进的点么?

  • thread对象创建的时候不可以设置为join(),因为join会阻塞主线程,主线程会在创建第一个线程后阻塞,无法接着创建线程
  • 这里的shutdown关闭线程池,虽然可以保证线程正在执行的任务可以结束,但是不能保证队列中的任务都执行
  • 线程的工作任务只能以std::function<>的形式传递,且无法传递有参数的任务

那么如何确保在主线程在工作线程之后销毁呢,可以修改线程池对象的析构函数,在析构函数中调用shutdown()函数即可。

不过上面的代码依然有一个问题,那就是如何传递含有参数的工作函数,每次调用之前都需要手动使用std::bind()未免有点麻烦了。参考开源的一个代码实现:

我们可以通过可变参数来解决上述问题,具体过程已在注释中解释了。

//这里是C++中的尾置返回,函数接受一个“万能引用”,左值/右值都可以接收,和一个可变参数模板
  template<typename F, typename...Args>
  auto submit(F&& f, Args&&... args) -> std::future<decltype(f(args...))> {
    // 使用std::bind把传递进来的函数和参数绑定为一个新的可调用对象
    // std::forward可以保持参数原本的类型
    std::function<decltype(f(args...))()> func = std::bind(std::forward<F>(f), std::forward<Args>(args)...);
    // 使用std::packaged_task包装一个可调用对象,可以异步的使用get_future()获取结果 
    auto task_ptr = std::make_shared<std::packaged_task<decltype(f(args...))()>>(func);

    // 使用匿名函数将wrok_func包装成一个无参数,无返回值的可调用对象
    std::function<void()> wrapper_func = [task_ptr]() {
      (*task_ptr)(); 
    };

    // 入队
    m_queue.enqueue(wrapper_func);

    // 唤醒一个被阻塞的线程
    m_conditional_lock.notify_one();

    // 返回结果
    return task_ptr->get_future();
  }

在解决了线程池的实现之后,还剩下最后一个问题。线程池的线程数目设置为多少比较合适?根据阻抗匹配原则。如果池中线程在执行任务时,密集计算所占的时间比重为P,而系统一共有C个CPU,为了让这C个CPU跑满而又不至于过载,线程池大小的经验公式T = C/P。