一、线程池是什么、解决什么问题?
线程池是维持管理一定数量线程的池式结构,与它类似的池式结构还有内存池、连接池,对象池,它们的共同点是对资源进行复用。
之所以要维持一定数量的线程是因为系统的资源有限,随着线程的数量一直增加可能不再带来性能的提升,甚至可能造成负担。同时也避免了重复创建销毁线程。
线程池异步执行耗时任务,不过度占用核心线程,充分利用多核,并发执行核心业务。在nginx 、redis中都有使用线程池。
为了充分利用系统资源,通常在选择线程池线程数量时,若为cpu密集型选择cpu核心数为线程数,若为io密集型选择2倍cpu核心数为线程数量。
二、线程池的构成
线程池由生产者线程、消费者线程和任务队列构成。生产者线程负责发布任务,往任务队列中push任务,消费者线程从任务队列取任务执行。
三、代码实现
1.面对生产者线程
发布任务到线程池Post
代码如下(示例):
class ThreadPool
{
public:
//初始化线程池
explicit ThreadPool(int threads_num){
for(int i = 0; i < threads_num; ++i){
workers_.emplace_back([this](void){Worker();});
}
}
//停止线程池
~ThreadPool(){
task_queue_.Cancel();
for(auto &worker : workers_){
if(worker.joinable()){
worker.join();//等待退出
}
}
}
//发布任务到线程池
void Post(std::function<void()> task){
task_queue_.Push(task); //push和pop都是线程安全的
}
private:
//入口函数 消费者线程不断弹出元素
void Worker(){
while (true){
std::function<void()> task;
if(!task_queue_.Pop(task)){
break;//退出
}
task();//正常弹出 执行任务
}
}
BlockingQueuePro<std::function<void()>> task_queue_;
std::vector<std::thread> workers_;
};
2.阻塞队列的设计
之所以选择队列是因为队列是一个双开口结构,生产者对应一个口,消费者对应另一个口,操作队列时间复杂度为O(1),加锁简单。单个阻塞队列模式适合单生产者多消费者模型。
代码如下(示例):
//线程安全的阻塞队列
template <typename T>
class BlockingQueue
{
public:
BlockingQueue(bool nonblock = false) : nonblock_(nonblock){}
//入队操作
void Push (const T &value){
//在对象构造的时候lock 对象结束稀构的时候自动unlock
std::lock_guard<std::mutex> lock(mutex_);
queue_.push(value);
not_empty_.notify_one();
}
//正常pop 弹出元素 消费者取数
//异常pop 没有弹出元素 退出
//通过bool判断是否正常 由参数引用取值
bool Pop(T &value){
//pop可能会阻塞 可能需要手动unlock 使用unique_lock
std::unique_lock<std::mutex> lock(mutex_);
//wait里面:
// 1. mutex_.unlock()
// 2. queue_empty && !nonblock 线程在 wait 中阻塞
// notify_one notify_all的时候 唤醒线程 通常线程就在这休眠
// 3. 假设满足notify_one notify_all条件 mutex_.lock()
// 4. 不满足条件 回到 2
not_empty_.wait(lock,[this]{return !queue_.empty() || nonblock_;});
if(queue_.empty()) return false;
value = queue_.front();
queue_.pop();
return true;
}
// 解除阻塞在当前队列的线程 退出的时候
void Cancel(){
std::lock_guard<std::mutex> lock(mutex_);
nonblock_ = true;
not_empty_.notify_all();
}
private:
bool nonblock_; //队列是否阻塞
std::queue<T> queue_; //使用队列先进先出时间复杂度o(1)
std::mutex mutex_; //队列是临界资源 需要加锁
std::condition_variable not_empty_; //条件变量 用来阻塞线程唤醒线程 用户定义队列为空就会阻塞线程
};
2.阻塞队列的优化
使用双队列结构,生产者对应一个队列,消费者对应一个队列,实现队列切换,减少了锁征用,适合多生产者线程多消费者线程模式。
template <typename T>
class BlockingQueuePro
{
public:
BlockingQueuePro(bool nonbolck = false) : nonblock_(nonbolck){}
void Push(const T &value){
std::lock_guard<std::mutex> lock(prod_mutex_);
prod_queue_.push(value);
not_empty_.notify_one();
}
bool Pop(T &value)
{
std::unique_lock<std::mutex> lock(cons_mutex_);
if(cons_queue_.empty() && SwapQueue_() == 0){
return false;
}
value = cons_queue_.front();
cons_queue_.pop();
return true;
}
void Cancel()
{
std::lock_guard<std::mutex> lock(prod_mutex_);
nonblock_ = true;
not_empty_.notify_all();
}
private:
int SwapQueue_(){
std::unique_lock<std::mutex> lock(prod_mutex_);
not_empty_.wait(lock,[this]{return !prod_queue_.empty() || nonblock_;});
std::swap(prod_queue_,cons_queue_);
return cons_queue_.size();
}
bool nonblock_;
std::queue<T> prod_queue_;
std::queue<T> cons_queue_;
std::mutex prod_mutex_;
std::mutex cons_mutex_;
std::condition_variable not_empty_;
};
此外线程池实现时还需注意将队列和线程池封装分离,使用前置声明代替头文件包含,避免循环依赖,导致改动小部分代码造成大量重新编译。
详细内容可参考www.0voice.com