先看头文件,构造函数的min和max是传入的最小和最大线程数量,thread::hardware_concurrency用于查询系统中可以并行执行的线程数量,也就是 CPU 核心数。有个添加任务的函数: void addTask(function<void(void)> task); 这个我们应该都知道,通过包装器打包成一个可调用对象task,返回值是和参数都是void。m_exitThread是当管理者线程判定需要销毁一些工作线程的时候,对应的要销毁的线程数量。
另外通常我们只需要一个线程池实例,所以可以把线程池的拷贝和赋值构造函数delete,把构造函数设置为私有,通过公共的getInstance接口可以获取唯一的线程池实例。
#pragma once
#include <atomic>
#include <condition_variable>
#include <functional>
#include <map>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>
using namespace std;
class ThreadPool {
public:
~ThreadPool();
//添加任务
void addTask(function<void(void)> task);
static ThreadPool &getInstance() {
static ThreadPool instance;
return instance;
}
private:
ThreadPool(int min = 2, int max = thread::hardware_concurrency());
ThreadPool(const ThreadPool &) = delete;
ThreadPool &operator=(const ThreadPool &) = delete;
private:
void manager(void);
void worker(void);
private:
thread *m_manager; //管理者线程
vector<thread::id> m_ids; //储存已经退出任务函数的线程
map<thread::id, thread> m_workers; //储存线程对象
atomic<int> m_minThread; //最小线程数量
atomic<int> m_maxThread; //最大线程数量
atomic<int> m_curThread; //当前线程数量
atomic<int> m_idleThread; //空闲线程数量
atomic<int> m_exitThread;
atomic<bool> m_stop; //线程池开关
queue<function<void(void)>> m_tasks; //任务队列
mutex m_queueMutex;
mutex m_idsMutex;
condition_variable m_condition;
};
再看源文件,在构造函数中创建工作线程以及管理者线程。当添加任务后,如果用的是notify_all来唤醒阻塞的工作线程取任务,会引发虚假唤醒问题,关于虚假唤醒,我前面的博客已经解决过了。
#include "ThreadPool.h"
#include <iostream>
ThreadPool::ThreadPool(int min, int max)
: m_minThread(min), m_maxThread(max), m_curThread(min), m_idleThread(min),
m_exitThread(0), m_stop(false) {
//创建管理者线程
m_manager = new thread(&ThreadPool::manager, this);
//创建工作线程
cout << "max:" << max << endl;
for (int i = 0; i < min; i++) {
thread t(&ThreadPool::worker, this);
//注意,线程对象不允许拷贝:thread(const thread&)=delete;
m_workers.insert(make_pair(t.get_id(), move(t)));
}
}
ThreadPool::~ThreadPool() {
m_stop = true;
m_condition.notify_all();
for (auto &it : m_workers) {
thread &t = it.second;
if (t.joinable()) {
cout << "析构函数执行!线程" << t.get_id() << "将要退出!" << endl;
t.join();
}
}
if (m_manager->joinable()) {
m_manager->join();
}
delete m_manager;
}
void ThreadPool::manager(void) {
while (!m_stop.load()) {
//每隔三秒检测一次子线程数量
this_thread::sleep_for(chrono::seconds(3));
int idel = m_idleThread.load();
int cur = m_curThread.load();
cout << "当前线程数量:" << cur << " 空闲线程数量:" << idel << endl;
//如果空闲线程数量大于总线程数量一半,销毁线程
if (idel > cur / 2 && cur > m_minThread) {
cout << "将要销毁线程" << endl;
//一次销毁两个线程
m_exitThread.store(2);
m_condition.notify_all();
// this_thread::sleep_for(chrono::seconds(1));
lock_guard<mutex> locker(m_idsMutex);
for (auto id : m_ids) {
auto it = m_workers.find(id);
if (it != m_workers.end()) {
cout << "线程" << (*it).first << "被管理者线程销毁了" << endl;
(*it).second.join();
//因为任务函数已经退出,所以Join会立即返回
// join等待子线程退出并回收资源,这个线程后续不可使用
// detach工作完成之后会自动释放资源,线程后续依然可以使用
m_workers.erase(it);
}
}
m_ids.clear();
} else if (idel == 0 && cur < m_maxThread) {
thread t(&ThreadPool::worker, this);
//注意,线程对象不允许拷贝:thread(const thread&)=delete;
m_workers.insert(make_pair(t.get_id(), move(t)));
m_curThread++;
m_idleThread++;
}
}
}
void ThreadPool::addTask(function<void(void)> task) {
{
lock_guard<mutex> locker(m_queueMutex);
m_tasks.emplace(task);
}
m_condition.notify_one();
}
void ThreadPool::worker(void) {
while (!m_stop.load()) {
function<void(void)> task = nullptr;
{
unique_lock<mutex> locker(m_queueMutex);
while (m_tasks.empty() && !m_stop.load()) {
m_condition.wait(locker);
if (m_exitThread > 0) {
m_exitThread--;
m_curThread--;
m_idleThread--;
cout << "线程" << this_thread::get_id() << "的工作函数先退出!"
<< endl;
lock_guard<mutex> locker(m_idsMutex);
m_ids.push_back(this_thread::get_id());
//在管理者线程释放被销毁的线程对象
return;
}
}
// m_condition.wait(locker, [&] { return !m_tasks.empty(); });
if (!m_tasks.empty()) {
cout << "取出了一个任务" << endl;
task = move(m_tasks.front());
m_tasks.pop();
}
}
if (task) {
m_idleThread--;
task();
m_idleThread++;
}
}
}
void calc(int x, int y) {
int z = x + y;
cout << "z=" << z << endl;
cout << endl << endl << endl;
this_thread::sleep_for(chrono::seconds(2));
}
int main() {
ThreadPool &pool = ThreadPool::getInstance();
for (int i = 0; i < 10; i++) {
auto obj = bind(calc, i, i * 2);
pool.addTask(obj);
}
getchar(); //阻塞主线程
return 0;
}
关于worker函数和manager函数的实现,代码注释已经解释的很清楚了,我就不多说了,最后测试的时候发现,管理者线程销毁线程的时候,结果并非预期。关于管理者销毁线程的机制我们知道,是设置m_exitThread,然后唤醒所有工作线程,当某两个工作线程率先唤醒,并且判定自己将退出的时候,把自身id记录到了vector容器,然后return退出,然后manager通过遍历这个容器获取要销毁的线程id,调用join进行资源回收,并打印 cout << "线程" << (*it).first << "被管理者线程销毁了" << endl;但结果不是这样:
可以看到,管理者线程没有销毁vector容器中的线程资源,而是在最后的析构函数销毁的,为什么会这样?其实当管理者线程调用notify_all之后,就继续往下执行,可能还没来得及等到worker线程把自身id放入vector,就已经遍历结束了。因此我们可以加一句:
把中间注释的那行代码放开,就解决了这个问题:
现在,同步线程池就完成了,接下来实现异步线程池,需要用c++11提供的一些异步的类,我也在之前的博客介绍过它们了。
首先改一下原来的calc任务函数:
(通过promise调用set_value拿到结果,然后调用get_future把结果传递给future,然后future调用get来获取结果)
(packaged_task
通过get_future与future对象关联,然后线程完成后可以通过future类获取结果)
因此promise和packaged_task
我们二选一就可以,在这里我选择packaged_task
。
因为我们改了任务函数,因此,我们还需要把void addTask(function<void(void)> task);
函数改一下,返回值就是future对象。
template <typename F, typename... Args> // F是一个可调用对象
auto addTask(F &&f, Args &&...args)
-> future<typename result_of<F(Args...)>::type>
{
using returnType = typename result_of<F(Args...)>::type;
//创建一个packaged_task对象
packaged_task<returnType()> pkg(
bind(forward<F>(f), forward<Args>(args)...));
//得到future
future<returnType> res = pkg.get_future();
//把任务函数添加到队列
m_queueMutex.lock();
m_tasks.emplace([pkg = move(pkg)]() mutable { pkg(); });
m_queueMutex.unlock();
m_condition.notify_one();
return res;
}
这代码一看有点复杂,其实并不复杂,都是我之前博客讲过的万能引用和完美转发。
F是一个可调用对象,Args是可变参数,addTask的参数是万能引用,返回值是->指向的东西,也就是一个future对象,future内部类型是我们用result_of推导出来的结果,也就是任务函数的结果的类型,把它简化命名为returnType,然后用包装器把一个返回值为returnType,参数为Args的函数也就是任务函数包装成pkg,(m_tasks.emplace([pkg = move(pkg)]() mutable { pkg(); });
代码并不会立即执行pkg任务函数,而是将一个可调用对象(即 lambda)放入任务队列中。) 然后返回这个future对象,以便后续通过get获取函数执行结果。
再看一个细节,为什么不用&捕获pkg?因为如果 pkg
在 lambda 执行时已经被移动或者线程池已经被销毁,这样的引用可能会导致未定义行为。当然也可以用智能指针封装一下,然后捕获这个智能指针也没问题。
已经正确地使用了 std::move
来移动 pkg
,但是编译还是报错了,因为 std::function
期望其目标类型是可以被拷贝构造的,而 std::packaged_task
不是。最终还是只能依靠指针上场:
template <typename F, typename... Args> // F是一个可调用对象
auto addTask(F &&f, Args &&...args)
-> future<typename result_of<F(Args...)>::type>
{
using returnType = typename result_of<F(Args...)>::type;
//创建一个packaged_task对象
// packaged_task<returnType()> pkg(
// bind(forward<F>(f), forward<Args>(args)...));
auto mytask = make_shared<packaged_task<returnType()>>(
bind(forward<F>(f), forward<Args>(args)...));
//得到future
future<returnType> res = mytask->get_future();
//把任务函数添加到队列
m_queueMutex.lock();
m_tasks.emplace([mytask]() mutable { (*mytask)(); });
m_queueMutex.unlock();
m_condition.notify_one();
return res;
}
编译成功了,接下来就是测试:
现在不需要阻塞主线程了,因为调用get会阻塞,一直到future对象里面有数据(任务函数执行的结果),现在异步线程池就完成了!