模型
源码分析
1)接口
class ThreadPool : noncopyable
{
public:
typedef std::function<void ()> Task;
explicit ThreadPool(const string& nameArg = string("ThreadPool"));
~ThreadPool();
void setMaxQueueSize(int maxSize) { maxQueueSize_ = maxSize; } // 设置队列大小
void setThreadInitCallback(const Task& cb) // 设置回调
{ threadInitCallback_ = cb; }
void start(int numThreads); // numThreads为要创建的子线程数目,开启线程
void stop(); // 关闭线程
const string& name() const
{ return name_; }
size_t queueSize() const;
void run(Task f); // 任务执行函数
};
2)成员变量
private:
bool isFull() const REQUIRES(mutex_);
void runInThread();
Task take();
mutable MutexLock mutex_; //互斥锁
Condition notEmpty_ GUARDED_BY(mutex_); // 条件变量: 非空判断
Condition notFull_ GUARDED_BY(mutex_); // 条件变量: 非满判断
string name_;
Task threadInitCallback_; // 回调函数
std::vector<std::unique_ptr<muduo::Thread>> threads_; // 线程组 使用智能指针维护单个线程对象
std::deque<Task> queue_ GUARDED_BY(mutex_); //双向队列 维护要执行的任务
size_t maxQueueSize_;
bool running_; //是否运行标志
3)线程启停
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);
threads_.emplace_back(new muduo::Thread(
std::bind(&ThreadPool::runInThread, this), name_+id)); //这里使用emplace_back(c++11)是为了提高插入性能,对比push_back
threads_[i]->start(); // 开启numThreads个线程加入线程池,等待有任务时进行调度
}
if (numThreads == 0 && threadInitCallback_)
{
threadInitCallback_();
}
}
void ThreadPool::stop()
{
{
MutexLockGuard lock(mutex_); // 线程停止时加锁
running_ = false;
notEmpty_.notifyAll();
notFull_.notifyAll();
}
for (auto& thr : threads_)
{
thr->join();
}
}
说明:线程开始时不需要加锁,是因为此时只是创建空线程,并没有具体的任务来进行调度,故为单线程,不存在数据被多线程改变的情况。stop的时候有共享数据的修改,故需要加锁。
4)存任务
//注意: 存任务时一般需要进行队列非满判断; 取任务时一般需要进行队列非空判断
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(); //非满通知 注意这里时notify(),而不是notifyAll()
}
}
5)取任务
ThreadPool::Task ThreadPool::take()
{
MutexLockGuard lock(mutex_);
// always use a while-loop, due to spurious wakeup
while (queue_.empty() && running_)
{
notEmpty_.wait();
}
Task task;
if (!queue_.empty())
{
task = queue_.front();
queue_.pop_front();
if (maxQueueSize_ > 0)
{
notFull_.notify();
}
}
return task;
}
这里主要关注一个虚假唤醒的问题:
while (queue_.empty() && running_)
{
notEmpty_.wait();
}
关于虚假唤醒,可以查看:muduo网络库——条件变量(condition variable)_却道天凉_好个秋的博客-CSDN博客
多个线程在同时等待queue不为空的条件发生。当有信号signalA通知queue不为空时,操作系统同时唤醒所有线程,如线程A竞争成功,执行下面的pop操作,使得queue再次为空。但其他线程此时也被唤醒,又由于线程A的操作导致queue再次为空,其他线程无法执行对应的操作,即被虚假唤醒。
6)子线程运行函数
void ThreadPool::runInThread()
{
try
{
if (threadInitCallback_)
{
threadInitCallback_();
}
while (running_) // 每一个线程都是一个loop 只有在stop的时候才会停止
{
Task task(take()); // 从队列中取任务
if (task)
{
task(); // 如果取到任务,则执行任务内容
}
}
}
...
}
测试
#include "muduo/base/ThreadPool.h"
#include "muduo/base/CountDownLatch.h"
#include "muduo/base/CurrentThread.h"
#include "muduo/base/Logging.h"
#include <stdio.h>
#include <unistd.h>
// 作为入队任务,有任务执行时打印当前线程id
void print()
{
printf("tid=%d\n", muduo::CurrentThread::tid());
}
void test(int maxSize)
{
LOG_WARN << "Test ThreadPool with max queue size = " << maxSize;
muduo::ThreadPool pool("MainThreadPool"); // 创建线程池
pool.setMaxQueueSize(maxSize); // 设置队列最大值
pool.start(5); // 开启5个子线程来进行调度
LOG_WARN << "Adding";
pool.run(print); // 入队
pool.run(print); // 入队
for (int i = 0; i < 100; ++i)
{
char buf[32];
snprintf(buf, sizeof buf, "task %d", i);
pool.run(std::bind(printString, std::string(buf)));
}
LOG_WARN << "Done";
muduo::CountDownLatch latch(1);
pool.run(std::bind(&muduo::CountDownLatch::countDown, &latch));
latch.wait();
pool.stop();
}
int main()
{
test(0);
test(1);
test(5);
test(10);
test(50);
}
总结
线程池为避免频繁创建、销毁线程,提供一组子线程,能从工作队列取任务、执行任务,而用户可以向工作队列加入任务,从而完成用户任务。
主要步骤如下:
1)创建一组线程,每个线程为一个loop循环;
2)有任务需要执行时,入队列;
3)当有线程池中有线程空闲时,从队列中取出任务执行回调;