C++简易线程池

最近作业刚好用到了多线程的内容,又重新写了一遍线程池,加深了对其的理解。这里基于C++11的thread来实现一个简单通用的线程池,基本思路是,构造函数里面创建一定数量的线程,所有线程共享一个任务队列,每个线程进入一个“死”循环,监听任务队列,一旦来了新的任务,则唤醒一个线程执行任务。

实现

线程池有几个关键的变量:

  • std::vector<std::thread> threads; — 保存所有的线程实例,用于析构函数时候销毁
  • std::queue<std::function<void(void)> > tasks; — 共享的任务队列,最好使用queue或者list等,头尾操作(如:push,pop)都是 O ( 1 ) O(1) O(1)
  • std::mutex mtx; — 全局锁,用于保护对于共享任务队列的访问
  • std::condition_variable cv; — 条件变量,用于唤醒线程

几个关键函数详解:

(1)添加任务

外部函数通过该函数添加任务到线程池内部,注意由于任务队列是所有线程共享的,所以这里添加任务之前,需要先上锁保证线程安全。该函数的最后一行this->cv.notify_one();,就是随机唤醒一个闲置线程来执行该任务(如果线程都在忙,则最先闲置下来的线程执行该任务)。

void addTask(std::function<void(void)> task) {
    std::unique_lock<std::mutex> lck(mtx);
    this->tasks.push(task);
    this->numTaskRemaining++;
    // envoke a thread to do the task
    this->cv.notify_one();
}

(2)线程任务

每个线程被创建后,就会执行该函数。函数一开始cv.wait,等待新的任务,或者线程池被销毁;一旦有新的任务来,则从任务队列里面获取一个任务,然后释放锁。因为在任务执行过程中,不涉及任何race condition,并且我们并不知道任务执行的时长(可能会很长),所以我们应该先释放锁,让其他线程可以访问共享变量。等待任务执行结束后,重新上锁,然后修改剩余任务数量。

void doTask() {
      while (true) {
        std::unique_lock<std::mutex> lck(this->mtx);
        // use a conditional variable to wait
        this->cv.wait(lck, [this] {
          // already in the critical section, so can access these variables safely
          return !this->tasks.empty() || this->stop;
        });
        if (this->stop) {
          return;
        }
        // fetch a task
        std::function<void(void)> task = std::move(this->tasks.front());
        this->tasks.pop();
        lck.unlock();
        // no need to lock while doing the task
        task();
        // lock again to update the remaing tasks variable
        lck.lock();
        this->numTaskRemaining--;
        // notify the waitAll()
        cv_finished.notify_one();
      }
}

(3)等待所有任务执行完毕

这里我额外实现了一个waitAll函数,可以等待任务队列里面所有的任务被执行完。注意这里是执行完,而不是队列为空,这两者不是一个概念。举个例子:任务队列里面只剩下最后两个任务,然后此时有两个线程都是空闲的,他们分别获取并执行一个任务,假设线程A执行任务A需要10s,线程B执行任务B需要1s;那么1s后线程B执行完任务,调用了cv_finished.notify_one();,此时任务队列已经是空,但是所有任务并没有全部执行完成(注意任务A还需要9s)。所以这里我用的是this->numTaskRemaining == 0;而不是this->tasks.empty();

void waitAll() {
    std::unique_lock<std::mutex> lck(mtx);
    this->cv_finished.wait(lck, [this] { return this->numTaskRemaining == 0; });
}

将上述代码结合起来,就得到了最终的完整版代码。

class ThreadPool {
   private:
    std::vector<std::thread> threads;
    std::queue<std::function<void(void)> > tasks;
    // global mutex, use to protext the task queue
    std::mutex mtx;
    std::condition_variable cv;
    std::condition_variable cv_finished;
    bool stop;
    size_t numTaskRemaining;

    void doTask() {
      while (true) {
        std::unique_lock<std::mutex> lck(this->mtx);
        // use a conditional variable to wait
        this->cv.wait(lck, [this] {
          // already in the critical section, so can access these variables safely
          return !this->tasks.empty() || this->stop;
        });
        if (this->stop) {
          return;
        }
        // fetch a task
        std::function<void(void)> task = std::move(this->tasks.front());
        this->tasks.pop();
        lck.unlock();
        // no need to lock while doing the task
        task();
        // lock again to update the remaing tasks variable
        lck.lock();
        this->numTaskRemaining--;
        // notify the waitAll()
        cv_finished.notify_one();
      }
    }

   public:
    ThreadPool(int cnt) : stop(false), numTaskRemaining(0) {
      // initialize the threadpool
      for (int i = 0; i < cnt; i++) {
        threads.push_back(std::thread([this] { doTask(); }));
      }
    }

    ~ThreadPool() {
      // first finish all remaining tasks
      waitAll();
      std::unique_lock<std::mutex> lck(mtx);
      this->stop = true;
      // notify all thread to finish
      this->cv.notify_all();
      lck.unlock();
      for (auto & th : threads) {
        if (th.joinable()) {
          th.join();
        }
      }
    }

    void addTask(std::function<void(void)> task) {
      std::unique_lock<std::mutex> lck(mtx);
      this->tasks.push(task);
      this->numTaskRemaining++;
      // envoke a thread to do the task
      this->cv.notify_one();
    }

    // This function will notify the threadpool to run all task until the queue is empty;
    void waitAll() {
      std::unique_lock<std::mutex> lck(mtx);
      this->cv_finished.wait(lck, [this] { return this->numTaskRemaining == 0; });
    }
  };

使用

注意我们的线程池接受的任务类型是function<void(void)>,就是没有参数没有返回值的一个函数,你可能会觉得这个限制很大,但其实我们可以用另一种方式将有参函数转换为无参函数,那就是c++11退出的lamda函数。

新建一个main.cpp, 写入如下代码:

#include <unistd.h>
#include <iostream>
#include "threadpool.hpp"

void printHello(int taskID, std::thread::id threadID) {
  std::cout << "This is task " << taskID << ", running on thread " << threadID << "\n";
}

int main(int argc, char * argv[]) {
  ECE565::ThreadPool tp(2);

  int num = 1;
  tp.addTask([=] {
    sleep(1);
    printHello(num, std::this_thread::get_id());
  });

  num = 2;
  tp.addTask([=] {
    sleep(3);
    printHello(num, std::this_thread::get_id());
  });

  tp.waitAll();
  return EXIT_SUCCESS;
}

我们可以用[=]{ // do anything you want }这样的形式,将有参函数转换为无参函数。其中等于号代表“捕获”当前所有的变量(值传递),可以换成&从而变为引用传递,也可以指定“捕获”具体的变量。

编译时添加-pthread flag,并指明c++11,如g++ -std=gnu++11 -pthread -o main main.cpp threadpool.hpp,运行即可看到结果。

This is task 1, running on thread 140593016403712
This is task 2, running on thread 140593008011008
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页