cond.wait()为什么要使用mutex变量来保持同步
本文将通过一个线程池实例来解释cond.wait()为什么需要使用mutex
代码如下,线程池使用生产者和消费者模式,使用一个队列来存储任务,addTask()函数供外部调用不断往队列中生产任务,子线程不断在队列中处理(消费)任务。其中使用condition_variable变量和mutex变量来保证互斥与同步。
#include "ThreadPool.h"
#include <iostream>
using JobFunc = std::function<void()>;
int ThreadPool::addTask(JobFunc&& func){
std::unique_lock<std::mutex>lock(mx_);
int ret = tasks_.push(func);
if(ret > 0){
std::cout << "添加任务成功,当前任务数为: "<< getTaskNum() << std::endl;
cond_.notify_one();
}
return ret;
}
void ThreadPool::start(){
if(!processing_){
std::unique_lock<std::mutex>lock(mx_);
processing_ = true;
threads_.reserve(numWorkers_);
for(int i=0;i<numWorkers_;i++){
threads_.emplace_back(new std::thread(std::bind(&ThreadPool::run,this)));
}
}
}
void ThreadPool::run(){
while(processing_){
//std::cout << "run" << std::endl;
JobFunc jf;
std::unique_lock<std::mutex>lock(mx_);
{
while(processing_ && tasks_.getTaskNum() == 0){
cond_.wait(lock);
}
if(!processing_ && tasks_.getTaskNum() == 0){
return;
}
jf = tasks_.pop();
}
if(jf)
jf();
}
}
void ThreadPool::stop(){
{
std::unique_lock<std::mutex>lock(mx_);
processing_ = false;
}
cond_.notify_all();//通知所有线程当前线程池要停止了,不要再接收新的任务.
for(auto & th:threads_){
std::cout << "线程 " << th->get_id() << "销毁" << std::endl;
th->join();
if(th != nullptr){
delete th;
th = nullptr;
}
}
std::cout << "当前线程已经全部销毁" << std::endl;
}
int ThreadPool::getTaskNum(){
return tasks_.getTaskNum();
ThreadPool::run()在使用cond_.wait()时结合了mutex变量,原因有3点:
1、首先要清楚的是,在进行while(processing_ && tasks_.getTaskNum() == 0)判断之前已经对muex加锁,在陷入到wait()内之后,会将当前线程阻塞,前面这一段时间的加锁,就是为了保证当前线程操作的原子性,如果当前线程为B,判断队列为空进入了while()循环内但是还没有陷入wait,而正当此时,一个线程A运行addTask()添加了新的任务,并notify_on(),但是此时B并没有阻塞,所以B会错过被唤醒。
2、当前线程陷入wait阻塞后,需要释放mutex,只有这样才能够保证addTask()获得到锁,才能够有机会notify()阻塞的线程。所以wait()必须获得mutex的使用权,并在阻塞后解锁。
3、当阻塞的线程被唤醒后,wait在返回前会先对mutex加锁,这也是wait()必须获得mutex使用权的原因之一,如果没有对mutex加锁,很可能出现线程虽然被唤醒,但是任务队列中已经空了的情况,造成后面的空指针错误。纵然是加了while(processing_ && tasks_.getTaskNum() == 0)循环,判断条件显示队列已经不为空,但是从判断完,到执行后面的语句(tasks_pop())存在临界区,比如在单核多线程的情况下,线程D在执行完while(processing_ && tasks_.getTaskNum() == 0),显示队列不为空跳出循环,此时当前线程的时间片已经用完,被挂起,而另一个线程C获得了CPU使用权,并将队列中唯一的一个任务拿走(tasks_pop()),当D再次获得时间片运行tasks_.pop()时,就会出现空指针错误。
总结来讲:mutex保证了多线程对共享数据段的互斥访问,cond保证了线程之间的快速响应;在进行变量判断前需要加锁,wait()将当前=线程放入到阻塞队列后要自动解锁,当被唤醒时需要先加锁,然后判断条件。另外,在进行notify之前需要加锁,notify()之后需要解锁。