原理部分:
线程池是什么?
简单的说,就是一个管理一定数量线程的池式结构。
线程池解决的问题?
解决了异步执行耗时任务(不过度占用核心线程),充分利用多核的特点。
耗时:耗时等待,耗时计算
线程池如何解决问题的?
以生产消费模型 生产者(线程)
搞清楚原理后,我们就开始手搓线程池了
首先,在纯C语言中,我们常常用struct结构体,和callback回调函数进行配合,在c++中引入了函数对象functional和lambda表达式来写线程池
1.基本框架(ThreadPool)
class ThreadPool {
public:
// 初始化线程池
explicit ThreadPool(int threads_num);
// 停止线程池
~ThreadPool();
// 发布任务到线程池
void Post(std::function<void()> task);
private:
//工作线程
void Worker();
//存储工作线程的容器
std::vector<std::thread> workers_;
//堵塞队列
std::unique_ptr<BlockingQueue<std::function<void()>>> task_queue_;
};
因为使用者只需要向线程池中Push任务,所以我们就只写Post函数对外就行了,然后还有我们的工作线程和存储工作线程的数组。
这时候我们就要考虑用什么容器来存储任务
我们应该能想到用队列(queue)来存储任务
很不错,但是队列不是线程安全的,所以这个时候我们需要自己进行实现一个线程安全的队列 ,我们叫它"堵塞队列"
2.任务队列的实现
template <typename T>
class BlockingQueue {
public:
BlockingQueue(bool nonblock = false) : nonblock_(nonblock) { }
// 入队操作
void Push(const T &value) {
std::lock_guard<std::mutex> lock(mutex_);
queue_.push(value);
not_empty_.notify_one();
}
// 正常 pop 弹出元素
// 异常 pop 没有弹出元素
bool Pop(T &value) {
std::unique_lock<std::mutex> lock(mutex_);
// 1. mutex_.unlock()
// 2. queue_empty && !nonblock 线程在 wait 中阻塞
// notify_one notify_all 唤醒线程
// 3. 假设满足条件 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_; //true代表不堵塞
std::queue<T> queue_;
std::mutex mutex_;
std::condition_variable not_empty_;
};
在Push和Pop时,加上相应的锁,以防临界资源同时被多个线程修改,再根据条件变量进行线程的堵塞。
第一代线程池实现了
#include <thread>
#include <functional>
#include <vector>
#include <queue>
#include <mutex>
#include <condition_variable>
template <typename T>
class BlockingQueue {
public:
BlockingQueue(bool nonblock = false) : nonblock_(nonblock){}
void push(const T& value) {
std::lock_guard<std::mutex> lock(mutex_);
queue_.push(value);
not_empty_.notify_one();
}
bool pop(T& value) {
std::unique_lock<std::mutex> lock(mutex_);
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_;
std::mutex mutex_;
std::condition_variable not_empty_;
};
class ThreadPool {
public:
explicit ThreadPool(int threads_num) {
task_queue_ = std::make_unique<BlockingQueuePro<std::function<void()>>>();
for (size_t i = 0; i < threads_num; ++i) {
workers_.emplace_back([this] {Worker(); });
}
}
~ThreadPool() {
task_queue_->Cancel();
for (auto& worker : workers_) {
if (worker.joinable())
worker.join();
}
}
void Post(std::function<void()> task) {
task_queue_->Push(task);
}
private:
void Worker() {
while (true) {
std::function<void()> task;
if (!task_queue_->Pop(task)) {
break;
}
task();
}
}
std::vector<std::thread> workers_;
std::unique_ptr<BlockingQueue<std::function<void()>>> task_queue_;
};
优化:
我们根据一代代码,会发现有时消费者和生产者会同时抢一个锁,锁的碰撞粒度太大了,这时候我们想到维护两个队列,生产者队列,和消费者队列
生产者线程只负责生产者队列,消费者线程只负责消费队列。
然后我们又会想到队列之中的任务,我们如何进行传递?
使用swap,当生产者队列中有任务时,消费者队列没任务时,这时将生产者队列里的任务移动到消费者队列中,此时,也是唯一一次两者会同时抢一把锁,锁的碰撞粒度大大的减小了。
template <typename T>
class BlockingQueuePro {
public:
BlockingQueuePro(bool nonblock = false) : nonblock_(nonblock) {}
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_;
};
测试代码:
#include <iostream>
#include <thread>
#include <vector>
#include <atomic>
#include <chrono>
// 包含 BlockingQueue 和 ThreadPool 的定义
#include "threadpool.h"
// 全局计数器,用于统计任务完成的数量
std::atomic<int> task_counter{0};
// 任务函数
void Task(int id) {
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟任务执行时间
std::cout << "Task " << id << " executed by thread " << std::this_thread::get_id() << std::endl;
task_counter++; // 任务完成,计数器加一
}
// 生产者线程函数
void Producer(ThreadPool& pool, int producer_id, int num_tasks) {
for (int i = 0; i < num_tasks; ++i) {
int task_id = producer_id * 1000 + i; // 生成唯一的任务ID
pool.Post([task_id]() {
Task(task_id);
}); // 提交任务到线程池
std::cout << "Producer " << producer_id << " posted task " << task_id << std::endl;
}
}
int main() {
const int num_producers = 4; // 生产者线程数量
const int num_tasks_per_producer = 10; // 每个生产者提交的任务数量
const int num_threads_in_pool = 2; // 线程池中的工作线程数量
ThreadPool pool(num_threads_in_pool); // 创建线程池
std::vector<std::thread> producers; // 生产者线程集合
// 启动生产者线程
for (int i = 0; i < num_producers; ++i) {
producers.emplace_back(Producer, std::ref(pool), i, num_tasks_per_producer);
}
// 等待所有生产者线程完成
for (auto& producer : producers) {
producer.join();
}
// 等待所有任务完成
while (task_counter < num_producers * num_tasks_per_producer) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
std::cout << "All tasks completed. Total tasks executed: " << task_counter << std::endl;
return 0;
}
Tip:要注意头文件引用过深,引用循环
从而引出了前置声明的概念
学习分享:https://github.com/0voice