在我们进行实际编程时,经常会碰到数量级大、耗时长的任务,尤其在网络服务中,可能有几万个用户同时访问你的服务器,不可能进来一个用户我们就开辟一个线程,这样做的后果是当用户量大的时候线程数量过多,线程间的调度效率就很低下了,反而会影响程序的效率。因此这时候,我们可以通过线程池,对一定数量的线程进行复用,提高应用的效率。线程池的结构是根据设计模式中的生产者消费者模式进行设计的,感兴趣的朋友可以百度学习一下。
这几天看了网上几篇的线程池实现代码,本篇文章主要参考以下博客内容:
https://www.jianshu.com/p/eec63026f8d0
上面这篇博客基于C++11新特性用100行代码实现了线程池,代码比较简洁易读,但是没有对任务队列的最大数量进行限制,以及在线程池中线程空闲线程为0时没有相应的处理。
本文主要根据参考博客实现以下功能:
1.管理一定数量的线程,当有任务commit进来的时候,唤醒池中的线程处理任务。
2.当线程池中空闲线程为0时,等待其他线程处理完毕,再进行线程唤醒
3.对任务队列最大数量进行限制,当任务队列满时,阻塞commit函数调用的线程
具体代码如下,代码中基本每一行都加了注释:
#include <vector>
#include <queue>
#include <thread>
#include <atomic>
#include <condition_variable>
#include <future>
#include <functional>
#include <stdexcept>
#include <QDebug>
#include <iostream>
#include <omp.h>
class threadpool
{
private:
using Task = std::function<void()>; //using 相当于typedef
std::vector<std::thread> m_threads;
std::queue<Task> m_tasks; //任务队列
std::mutex m_lock;
std::condition_variable m_cvTask; //条件变量
std::atomic<bool> m_stoped; //是否关闭提交 std::atomic<T> 对于对象的操作都是原子的不用加锁
std::atomic<int> m_idlThrNum; //空闲线程数量
//std::atomic<bool> m_stoped;
std::atomic<int> m_taskNum;
std::atomic<int> m_maxTaskNum; //任务队列的最大任务数量
public:
inline threadpool(unsigned short size = 4,int maxTask = 500) :m_stoped(false)
{
m_idlThrNum = size < 1 ? 1 : size; //如果传入的构造线程数量为1以下,那默认为1
m_maxTaskNum= maxTask< 1 ? 1 : maxTask;
m_taskNum.store(0);
for (size = 0; size < m_idlThrNum; ++size)
{
m_threads.emplace_back([this]{
while(!this->m_stoped)//如果关闭为假,执行循环
{
Task task;
{
std::unique_lock<std::mutex> lock(this->m_lock);
this->m_cvTask.wait(lock,[this]{
//当停止为真或者任务队列不为空时信号量触发,取消阻塞状态
return this->m_stoped.load() || !this->m_tasks.empty();
});
//当信号量触发前一直阻塞在这里
if(this->m_stoped && this->m_tasks.empty())
return;//当触发线程池停止时,任务队列也为空,就结束线程
task = std::move(this->m_tasks.front()); // 取一个 task
this->m_tasks.pop();
}
m_idlThrNum--; //std::atomic<int> 是原子的,因此这里不用加锁
task();
m_idlThrNum++;
}
});
}
//单独的线程用于通知线程池执行任务
std::thread m_notifyThread([this]{
while(!this->m_stoped.load())
{
if(m_idlThrNum.load()>0&&m_taskNum.load()>0){
//m_taskNum--;
m_taskNum.store(m_taskNum.load()-1);
m_cvTask.notify_one(); // 唤醒一个线程执行
printf("---------------notify_one------------- \n");
}
std::this_thread::yield();
}
});//通知线程
m_notifyThread.detach();
}
inline ~threadpool()
{
m_stoped.store(true);
m_cvTask.notify_all(); // 唤醒所有线程执行
for (std::thread& thread : m_threads) {
//thread.detach(); // 让线程“自生自灭”
if(thread.joinable())
thread.join(); // 等待任务结束, 前提:线程一定会执行完
}
}
template<class F, class... Args>
auto commit(F&& f, Args&&... args) ->std::future<decltype(f(args...))>
{
if (m_stoped.load()){
throw std::runtime_error("commit on ThreadPool is stopped.");
}
using RetType = decltype(f(args...)); //获取函数f的返回值类型
auto task = std::make_shared<std::packaged_task<RetType()> >(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
//如果任务队列已达到最大容量,阻塞线程,等待历史任务处理
while(m_taskNum.load()>m_maxTaskNum.load())
{
//std::this_thread::yield();
std::this_thread::sleep_for(std::chrono::milliseconds(4));
}
std::future<RetType> future = task->get_future();
{
std::lock_guard<std::mutex> lock(m_lock);
m_tasks.emplace(
[task]()
{
(*task)();
}
);
}
// qDebug()<<"commit one!current task num: "<<m_taskNum;
m_taskNum++;
//m_cvTask.notify_one(); // 唤醒一个线程执行
return future;
}
//空闲线程数量
int idlCount()
{
return m_idlThrNum;
}
};
代码中包含了一些C++11的新特性:
1.使用匿名函数lambda表达式,让程序更加简洁,格式如下
//格式如下:
[捕获列表] (参数表){
//代码段
}
//例如
auto fun =[](){
std::cont<<"this is lambda!"<<std::endl;
}
fun();//即可调用函数
深入了解可自行百度。
2.std::atomic 模板类的对象操作都是原子的,因此本文我们对m_stoped、m_idlThrNum等多线程中共享的资源不需要进行加锁。
3.std::condition_variable信号量的使用,信号量调用wait()函数时,可以将线程阻塞在wait处,等待notify_one()或者notify_all()唤醒。
std::unique_lock<std::mutex> lock(this->m_lock);
this->m_cvTask.wait(lock,[this]{
//当停止为真或者任务队列不为空时信号量触发,取消阻塞状态
return this->m_stoped.load() || !this->m_tasks.empty();
});
//
m_cvTask.notify_one();//唤醒一个线程
//在调用notify_one()后,如果this->m_stoped.load() || !this->m_tasks.empty()为真
//即可唤醒线程
4.还有一些std::future,std::bind以及可变模板参数的写法,想要深入了解可以自行百度。
参考博客中,对于线程池中空闲线程为0的情况没做处理,也就意味着当线程都忙的情况下,我们再提交任务不会有线程对任务进行处理,这样肯定不符合实际应用要求,因此这里我们通过单独开辟一个通知线程,进行线程唤醒工作,这里为什么单独开辟线程,一是不想再线程忙的情况下阻塞commit线程(也可在m_idlThrNum为0时,阻塞commit()线程,但是由于一般线程池中线程数量较少,需要提交的任务多的情况下阻塞线程时间会较长,体验感不好),具体实现如下:
//单独的线程用于通知线程池执行任务
std::thread m_notifyThread([this]{
while(!this->m_stoped.load())
{
if(m_idlThrNum.load()>0&&m_taskNum.load()>0){
//m_taskNum--;
m_taskNum.store(m_taskNum.load()-1);
m_cvTask.notify_one(); // 唤醒一个线程执行
printf("---------------notify_one------------- \n");
}
std::this_thread::yield();
}
});//通知线程
m_notifyThread.detach();
这里再循环中我们使用std::this_thread::yield(),这地方是有门道的,std::this_thread::yield():当前线程放弃执行,操作系统调度另一线程继续执行。即当前线程将未使用完的“CPU时间片”让给其他线程使用,等其他线程使用完后再与其他线程一起竞争"CPU"。因此这里使用std::this_thread::yield()会比std::this_thread::sleep_for(std::chrono::milliseconds(3));好一点。大家可以自行百度一下,如果我理解错了麻烦评论告诉我。
接下来编写代码对线程池进行测试:
void fun1(int slp){
printf(" hello, fun1 ! %d\n" ,std::this_thread::get_id());
std::this_thread::sleep_for(std::chrono::milliseconds(slp*1000));
printf("sleep end\n");
}
struct gfun {
int operator()(int n) {
for(int i=0;i<5;i++){
lock.lock();
printf("%d hello, gfun ! %d\n" ,n, std::this_thread::get_id() );
lock.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(2000));
lock.lock();
printf("%d hello, gfun ! %d\n" ,n, std::this_thread::get_id() );
lock.unlock();
}
return 42;
}
};
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
threadpool thrPool(4,5);
std::cout << " ======= main thread ========= " << std::this_thread::get_id() << std::endl;
// std::future<void> ff = thrPool.commit(fun1,10);
// std::future<int> fg = thrPool.commit(gfun(),10);
// //ff.get();
// std::cout <<"get end"<< std::endl;//<<fg.get()
for(int i=0;i<20;i++)
{
thrPool.commit(fun1,5);
}
printf("=======get end=======\n");
return a.exec();
}
以上测试代码是在Qt的控制台程序中写的,因此会出现QCoreApplication ,可以忽略,只看关键代码,将关键代码拷贝到VS中也可运行。
运行结果如下:
======= main thread ========= 1
---------------notify_one-------------
---------------notify_one-------------
hello, fun1 ! 2
hello, fun1 ! 3
---------------notify_one-------------
hello, fun1 ! 4
---------------notify_one-------------
hello, fun1 ! 5
sleep end
---------------notify_one-------------
hello, fun1 ! 2
sleep end
---------------notify_one-------------
hello, fun1 ! 3
sleep end
---------------notify_one-------------
hello, fun1 ! 4
sleep end
---------------notify_one-------------
hello, fun1 ! 5
sleep end
---------------notify_one-------------
hello, fun1 ! 2
sleep end
hello, fun1 ! 3
sleep end
---------------notify_one-------------
hello, fun1 ! 4
sleep end
hello, fun1 ! 5
sleep end
---------------notify_one-------------
hello, fun1 ! 2
sleep end
hello, fun1 ! 3
sleep end
---------------notify_one-------------
hello, fun1 ! 4
sleep end
hello, fun1 ! 5
sleep end
---------------notify_one-------------
hello, fun1 ! 2
sleep end
---------------notify_one-------------
hello, fun1 ! 3
=======get end=======
sleep end
sleep end
hello, fun1 ! 4
---------------notify_one-------------
hello, fun1 ! 5
sleep end
---------------notify_one-------------
---------------notify_one-------------
---------------notify_one-------------
sleep end
---------------notify_one-------------
sleep end
---------------notify_one-------------
sleep end
本篇博客是博主学习线程池后的记录,水平有限,如果上文有出现理解错误的地方,大家可以评论告诉我,感谢。