最近作业刚好用到了多线程的内容,又重新写了一遍线程池,加深了对其的理解。这里基于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