来自:华为C++一面:手撕线程池_哔哩哔哩_bilibili
华为海思:
手撕线程池
相关概念参考:
什么是线程池
维持管理一定数量线程的池式结构。
核心思想:线程复用。 避免频繁地创建和销毁线程带来的开销。
为什么需要线程池
线程池的工作流程
核心为生产者-消费者模型
线程池需要维护工作线程(消费者线程)和一个任务队列,生产者线程创建任务放入线程池的任务队列,消费者线程从任务队列中取出任务执行。
一个线程池包含:
任务队列使用一个手动实现的阻塞队列来实现;
工作线程使用一个线程vector
来实现。
BlockingQueuePro<std::function<void()>> task_queue_; // 任务队列
std::vector<std::thread> workers_; // 工作线程列表
工作线程函数是一个不断循环的函数,从任务队列中取出任务并执行
// 工作线程函数
void Worker() {
while (true) {
std::function<void()> task;
if (!task_queue_.Pop(task))
break;
task(); // 执行任务
}
}
构造函数传入一个整数作为线程池最大线程数,然后创建该数量的线程
// 构造函数
explicit ThreadPool(int num_threads) {
for (size_t i = 0; i < num_threads; i++) {
workers_.emplace_back([this] { Worker(); });
}
}
析构函数将阻塞队列设置为非阻塞模型,并阻塞当前线程等待所有工作线程执行完毕
// 析构函数
~ThreadPool() {
task_queue_.Cancel();
for (auto &worker : workers_) {
if (worker.joinable()) {
worker.join();
}
}
}
Post函数传入一个可调用对象和参数,将可调用对象和参数绑定之后加入到工作队列中。
// 添加任务
template <typename F, typename... Args>
void Post(F &&f, Args &&...args) {
auto task = std::bind(std::forward<F>(f), std::forward<Args>(args)...);
task_queue_.Push(task);
}
class ThreadPool {
public:
// 构造函数
explicit ThreadPool(int num_threads) {
for (size_t i = 0; i < num_threads; i++) {
workers_.emplace_back([this] { Worker(); });
}
}
// 析构函数
~ThreadPool() {
task_queue_.Cancel();
for (auto &worker : workers_) {
if (worker.joinable()) {
worker.join();
}
}
}
// 添加任务
template <typename F, typename... Args>
void Post(F &&f, Args &&...args) {
auto task = std::bind(std::forward<F>(f), std::forward<Args>(args)...);
task_queue_.Push(task);
}
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_; // 工作线程列表
};
阻塞队列是一种特殊的队列,同样遵循“先进先出”的原则,支持入队操作和出队操作。在此基础上,阻塞队列会在队列已满或队列为空时陷入阻塞,使其成为一个线程安全的数据结构,它具有如下特性:
(引用参考:阻塞队列(超详细易懂)-CSDN博客)
生产者和消费者共用一个队列和互斥锁
源码:
template <typename T>
class BlockingQueue {
public:
BlockingQueue(bool nonblock = false) : nonblock_(nonblock) {}
// 添加任务
void Push(const T &task) {
std::lock_guard<std::mutex> lock(mutex_);
queue_.push(task);
not_empty_.notify_one(); // 通知一个等待的线程
}
// 获取任务
bool Pop(T &task) {
std::unique_lock<std::mutex> lock(mutex_);
not_empty_.wait(lock, [this] { return !queue_.empty() || nonblock_; });
if (queue_.empty())
return false;
task = 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::mutex mutex_; // 互斥锁
std::condition_variable not_empty_; // 条件变量,队列为空时线程休眠
std::queue<T> queue_; // 任务队列
};
生产者和消费者有各自的任务队列和互斥锁
当消费者队列为空时,会尝试与生产者队列交换
若交换中生产者队列为空,使工作线程进入休眠;
若队列被设置为非阻塞,生产者队列为空,交换后消费者队列仍为空,此时会结束工作线程。
源码:
// 升级版队列,多生产者和多消费者
template <typename T>
class BlockingQueuePro {
public:
BlockingQueuePro(bool nonblock = false) : nonblock_(nonblock) {}
// 添加任务
void Push(const T &task) {
std::lock_guard<std::mutex> lock(producer_mutex_);
producer_queue_.push(task);
not_empty_.notify_one(); // 通知一个等待的线程
}
// 获取任务
bool Pop(T &task) {
std::unique_lock<std::mutex> lock(consumer_mutex_);
// 如果消费者队列为空,尝试交换生产者队列
if (consumer_queue_.empty() && SwapQueue_() == 0) {
return false; // 如果交换后仍然为空,则返回false
}
task = consumer_queue_.front();
consumer_queue_.pop();
return true;
}
// 解除阻塞当前队列的线程
void Cancel() {
std::lock_guard<std::mutex> lock(producer_mutex_);
nonblock_ = true; // 设置为非阻塞状态
not_empty_.notify_all(); // 通知所有等待的线程
}
private:
// 交换生产者队列到消费者队列
size_t SwapQueue_() {
std::unique_lock<std::mutex> lock(producer_mutex_);
not_empty_.wait(lock, [this] { return !producer_queue_.empty() || nonblock_; });
std::swap(producer_queue_, consumer_queue_); // 交换队列
return consumer_queue_.size(); // 返回新的消费者队列大小
}
bool nonblock_; // 是否为非阻塞模式
std::mutex producer_mutex_; // 生产者互斥锁
std::mutex consumer_mutex_; // 消费者互斥锁
std::condition_variable not_empty_; // 条件变量,队列为空时线程休眠
std::queue<T> producer_queue_; // 生产者任务队列
std::queue<T> consumer_queue_; // 消费者任务队列
};
Task()
:线程池中工作线程需要执行的任务Producer()
:将num_tasks
个任务添加到线程池中producers
:包含多个生产者,同时并行生成任务到线程池中#include
#include
#include
#include
#include
#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, task_id);
std::cout << "Producer " << producer_id << " posted task " << task_id << std::endl;
}
}
int main() {
const int num_producers = 3; // 生产者数量
const int tasks_per_producer = 5; // 每个生产者生成的任务数量
const int num_threads = 4; // 线程池中的线程数量
ThreadPool pool(num_threads); // 创建线程池
// 启动多个生产者线程
std::vector<std::thread> producers;
for (int i = 0; i < num_producers; i++) {
producers.emplace_back(Producer, std::ref(pool), i, tasks_per_producer);
}
// 等待所有生产者完成
for (auto &producer : producers) {
producer.join();
}
// 等待一段时间以确保所有任务都被处理完
while (task_counter < num_producers * tasks_per_producer) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
std::cout << "Total tasks executed: " << task_counter.load() << std::endl;
return 0;
}
条件变量与线程同步问题
C++ 条件变量:wait、wait_for、wait_until_c++ 条件变量 wait-CSDN博客
虚假唤醒问题
一般为操作系统层面的原因导致的
futex
)为了提高性能,允许在未收到信号时唤醒线程。例如:
解决方法
// 循环检查
while (condition) {
cond.wait(lock);
}
// 谓词
cond.wait(lock, [](return ready));
引用包装
【C++】引用包装(std::ref与std::cref)-CSDN博客
progschj/ThreadPool: A simple C++11 Thread Pool implementation
配合以下内容食用:
C++知识点记录-CSDN博客