线程池基于互斥锁,条件变量实现, 符合生产者消费者模型
生产者-消费者模型:
一种常见的并发设计模式,生产者负责产出资源放入缓存区中,消费者则负责从缓存区中获取资源。在线程池的实现过程中,资源则是任务,而消费者是不同的线程。线程从队列中获取任务以后完成。在这个过程中,任务队列需要用互斥锁进行保护,而线程之间的协调则需要条件变量实现。
条件变量:
条件变量用于在线程之间相互通知消息,实现对锁的持有和控制。接受线程会等待发送线程的通知,得到通知以后会继续进行后面的代码,某种程度上也起到一个阻塞的作用,等待生产者放出资源,具体实现思路如下:
(图片来自b站视频,侵权自删)
首先是接受线程等待发送线程写入资源,这个阶段中,接受县城持有互斥锁,并且被条件变量所阻塞,暂时不能写入共享数据。
然后是发送线程写入数据,这个时候是发送线程持有互斥锁,进行写入以后通知条件变量
完成写入以后,接收线程再持有互斥锁,结束阻塞状态继续进行
具体的代码使用如下:
主要有三个api进行使用
wait,notify_one以及notify_all;
//消费者部分
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock,[this]{return false;}); //第二个参数为初始阻塞判断,如果为空则为阻塞状态
//生产者任务
cv.notify_one();//通知一个线程解决问题
cv.notify_all();//通知所有线程解决问题
其实如果完全看代码的话,只需要当成一个线程之间阻塞方法即可
线程池类的代码实现
class ThreadsPool{
public:
//线程数组
std::vector<std::thread> threads;
//任务队列
std::queue<std::function<void()>> tasks;
//标志
bool stop;
//互斥锁
std::mutex mtx;
//条件变量
std::condition_variable cv;
//构造函数
//构造函数需要实现的功能就是:启动每一个线程,并且用条件变量等待任务队列的空缺
ThreadsPool(int num):stop(false){
for(int i=0;i<num;i++){
//emplace_back可以理解为直接调用构造函数
threads.emplace_back([this,i]{
while(1){
//创建锁
std::unique_lock<std::mutex> lock(this->mtx);
//启动条件变量, 如果程序已经停止执行或者任务队列不为空,则可以继续执行,否则就进行等待
this->cv.wait(lock,[this]{return stop||!tasks.empty();});
//如果是程序停止了, 就消除这个线程
if(stop){
return;
}
//从队列中取出任务, 并且进行执行,
std::function<void()> task(std::move(tasks.front()));
tasks.pop();
//已经取出, 取消锁定, 并且执行任务
lock.unlock();
task();
cout<<"线程"<<i<<"进行服务"<<endl;
}
});
}
}
//系出构造函数
~ThreadsPool(){
//设置标志位为停止
{
std::unique_lock<std::mutex> lock(this->mtx);
this->stop=true;
}
//通知所有线程启动,完成所有任务
cv.notify_all();
//等待所有线程结束,等待所有线程结束
for(auto& t : threads){
t.join();
}
}
//向线程池中加入任务的方法
template<typename Func, typename... Args>
void enqueue(Func func,Args... args){
//放入一个任务
{
std::unique_lock<std::mutex> lock(this->mtx);
tasks.emplace([=](){func(args...);});
}
//通知条件变量,启动一个线程来执行任务
cv.notify_one();
}
};
案例如图所示
补充:
用到了一些cpp11的语法,可能不太常用
lambad表达式:写法为
[]()->returntype{}
//[]为捕获参数,如果是[=]则代表捕获上下文中所有参数
//()为传入参数
但是其实也可以包括一些简写类型
比如
[]();
emplace相比于push,是直接调用构造函数
move用作移动,可以提升一些性能呢个