1、ThreadPool的成员变量:
/*
我们知道,如果类的成员函数不会改变对象的状态,那么这个成员函数一般会声明成const的。
但是,有些时候,我们需要在const的函数里面修改一些跟类状态无关的数据成员,那么这个数据成员就应该被mutalbe来修饰。
*/
mutable MutexLock mutex_;
Condition notEmpty_ GUARDED_BY(mutex_); //GUARDED_BY, 由...守护
Condition notFull_ GUARDED_BY(mutex_);
string name_;
Task threadInitCallback_;
std::vector<std::unique_ptr<muduo::Thread>> threads_; //保存所有线程
std::deque<Task> queue_ GUARDED_BY(mutex_); //deque形式的任务队列
size_t maxQueueSize_; //队列最大数量, 在start之前必须得设置。不是自由增长的。
bool running_; //线程运行的标识。while(running
重点两个:
1. 任务队列,采用deque双端队列,Task为函数包装器,可以给线程池任意函数让他帮你去run。
2.线程容器,维护所有线程的对象。
其他:
1.条件变量notEmpty_和 notFull_和mutex_配合使用,为任务队列的线程同步使用。
2.mutable(可变的)修饰mutex锁,因为mutex跟类的状态无关,所以我们期待在const函数中使用它。
3.threadInitCallback,线程池初始化时,我们需要自定义初始化的一些内容,可忽略。
4.maxQueueSize_限制队列的最大数量。需要在线程池start之前设置。
总的来说:所有东西都是为了维护任务队列和线程容器服务的。
2、线程池启动
void ThreadPool::start(int numThreads)
{
assert(threads_.empty());
running_ = true;
threads_.reserve(numThreads); //线程容器容量重置
for (int i = 0; i < numThreads; ++i)
{
char id[32];
snprintf(id, sizeof id, "%d", i+1);
/*
push_back():先调用构造函数构造出这个临时对象,最后调用移动构造函数将这个临时对象放入容器中.
emplace_back:在容器尾部添加一个元素,调用构造函数原地构造,不需要触发拷贝构造和移动构造。因此比push_back()更加高效
*/
threads_.emplace_back(new muduo::Thread(
std::bind(&ThreadPool::runInThread, this), name_+id));
threads_[i]->start();
}
if (numThreads == 0 && threadInitCallback_)
{
threadInitCallback_();
}
}
主要做了一件事:创建指定线程数量放入线程容器,并启动。 线程函数为runInThread。
3、线程函数runInThread(核心)
void ThreadPool::runInThread()
{
try
{
if (threadInitCallback_)
{
threadInitCallback_();
}
while (running_)
{
//阻塞在tack中的条件变量。
Task task(take());
if (task)
{
task();
}
}
}
//此处省略catch异常代码。
}
1. 在整个线程池running状态下,不断从tack()中拿取任务函数来执行。怎么拿的才是重点。
ThreadPool::Task ThreadPool::take()
{
MutexLockGuard lock(mutex_);
// always use a while-loop, due to spurious wakeup
/*
1.多核处理器,pthread_cond_signal可能激或多个线程,这种叫做虚假唤醒。if改成while判断可以在wait前后各判断一次。
*/
while (queue_.empty() && running_)
{
notEmpty_.wait(); //等待状态会挂起,同时释放互斥锁。为什么有锁?有一个数据,被两个线程同时wait拿走了。同步错误。
}
Task task;
if (!queue_.empty())
{
task = queue_.front();
queue_.pop_front();
if (maxQueueSize_ > 0)
{
notFull_.notify();
}
}
return task;
}
2. 利用条件变量notEmpty_, 等待任务队列有任务。没有任务咱就让线程挂起到这里,等用户从别的地方加入任务唤醒咱们就可以了。(此处有一个虚假唤醒问题,见下文补充)。
3. 从任务队列中pop一个任务出来,并返回。此时因为拿出来了一个任务,队列肯定不满,并且此时还是加锁状态,确保拿走后notfull成立。
以上就是线程池最核心内容(线程池的启动和运行)
4 、用户加入任务到线程池
用户只管直接调用 ThreadPool::run(task)。线程池自动为用户找到合适的线程去跑你的任务代码。
void ThreadPool::run(Task task)
{
//线程池没有线程,直接主线程执行。
if (threads_.empty())
{
task();
}
else
{
MutexLockGuard lock(mutex_);
while (isFull() && running_)
{
notFull_.wait();
}
if (!running_) return;
assert(!isFull());
queue_.push_back(std::move(task));
notEmpty_.notify();
}
}
1.使用 notFull_条件变量等待任务队列非满状态, 条件成立后加入到任务队列中。 此时肯定非空,通知其他线程开始任务队列有数据了,去作业吧。
补充:
条件变量
- 一个线程等待某个条件为真,而将自己挂起;另一个线程使得条件成立,并通知等待的线程继续。为了防止竞争(两个线程同时wait拿走同一个数据),条件变量的使用总是和一个互斥锁结合在一起。
- 当一个线程处于等待条件变量(condition variable)时,该线程不再占用互斥量(monitor),让其他线程能够进入互斥区去改变条件状态。
两种操作:
- 等待(wait):一个线程因为等待断言(assertion) P为真而处于等待在条件变量上,此时线程不会占用互斥量(monitor);
- 通知(signal/notify):另一个线程在使得断言(assertion) P为真的时候,通知条件变量。
存在的问题:虚假唤醒
- Linux中帮助中提到的:在多核处理器下,pthread_cond_signal可能会激活多于一个线程(阻塞在条件变量上的线程),结果是,当一个线程调用pthread_cond_signal()后,多个调用pthread_cond_wait()或pthread_cond_timedwait()的线程返回。这种效应成为”虚假唤醒。虽然虚假唤醒在pthread_cond_wait函数中可以解决,为了发生概率很低的情况而降低边缘条件(fringe condition)效率是不值得的,纠正这个问题会降低对所有基于它的所有更高级的同步操作的并发度。所以pthread_cond_wait的实现上没有去解决它。
解决办法:将条件的判断从if 改为while:
pthread_cond_wait中的while()不仅仅在等待条件变量前检查条件变量,实际上在等待条件变量后也检查条件变量。这样对condition进行多做一次判断,即可避免“虚假唤醒”