cpp-tbox项目链接 https://gitee.com/cpp-master/cpp-tbox
更多精彩内容欢迎关注微信公众号:码农练功房
往期精彩内容:
Linux应用框架cpp-tbox之弱定义
Linux应用框架cpp-tbox之日志系统设计
Linux应用框架cpp-tbox之事件驱动EventLoop
Linux应用框架cpp-tbox之事件驱动Event
整体结构图
下图是线程池的代码模型:
从模型可知,线程池被抽象成了ThreadPool类,其生命周期由ContextImp控制。
ThreadPool依赖于Loop对象,这是为了向IO线程派发任务。
ThreadPool和Data的关系,形如以下代码:
class MyClass {
private:
struct Data; // 声明嵌套的结构体类型
Data* d_; // 声明指针成员,但不在此初始化
public:
MyClass() : d_(nullptr) {} // 在构造函数的成员初始化列表中初始化d_为nullptr
// 其他成员函数、析构函数等...
};
这种写法的好处主要包括以下几点:
- 封装性:通过将Data结构体定义为私有成员,外界无法直接访问Data的内部细节,仅能通过类提供的公共接口来间接操作,增强了数据的封装性和安全性。
- 控制访问与操作隔离:类的设计者可以控制对Data结构体的直接访问和修改仅限于类内部,避免了外部误操作引起的数据不一致性问题。通过成员函数(如getter/setter)来操作d_,可以添加逻辑校验和控制。
- 灵活性与扩展性:将来如果Data结构体需要修改,只需在类内部进行,不会影响到使用该类的外部代码。此外,类可以通过添加新的操作函数来扩展对外接口,而无需更改外部代码对Data的引用方式。
- 内存管理:通过指针d_管理Data实例的生命周期,可以在类的构造函数中分配内存,在析构函数中释放,这样可以更好地控制内存的分配和释放,减少内存泄漏的风险。当然,这也要求开发者需要管理好资源,避免悬挂指针和内存泄露。
- 性能:直接操作指针相比于对象实例,减少了拷贝的开销(尤其是在大型对象或复杂结构体时),可以提高性能。另外,对于某些特定操作,直接操作指针可能提供更大的灵活性和效率。
创建
通过ThreadPool::initialize接口,完成线程池内工作线程的创建。工作线程个数可以在启动配置文件中指定:
{
"thread_pool": {
"max": 5,
"min": 2
}
}
初始化时,按照thread_pool.min个数来创建工作线程。
thread_pool.max为最大创建工作线程个数,如果空闲线程不够分配未认领的任务,且工作线程个数未超最大上限,还可以再创建新的线程。
每个工作线程都会被分配一个ThreadToken作为标识:
using ThreadToken = cabinet::Token;
这里使用using取别名,提高了代码的表现力。
对于cabinet::Token的实现先按下不表,这里只要知道是一个标识就够了,不脱离此次主线。
工作线程创建完成后,使用条件变量让工作线程挂起,直到有要执行的任务为止。
void ThreadPool::threadProc(ThreadToken thread_token)
{
LogDbg("thread %u start", thread_token.id());
while (true) {
Task* item = nullptr;
{
std::unique_lock<std::mutex> lk(d_->lock);
// ......
//! 使用条件变量等待任务
++d_->idle_thread_num;
d_->cond_var.wait(lk, std::bind(&ThreadPool::shouldThreadExitWaiting, this));
--d_->idle_thread_num;
// ......
}
执行任务
和工作线程类似,每个任务都会被分配一个TaskToken作为标识:
using TaskToken = cabinet::Token;
ThreadPool内的任务支持优先级,任务会按照优先级进入不同任务队列,数据结构如下:
// undo_tasks_cabinet对象为任务分配TaskToken
cabinet::Cabinet<Task> undo_tasks_cabinet;
// undo_tasks_token为优先级队列
std::array<std::deque<TaskToken>, THREAD_POOL_PRIO_SIZE> undo_tasks_token;
通过ThreadPool::execute接口,可以派发任务,在任务按照优先级进入对应队列后,使用条件变量,唤醒一个等待的工作线程:
ThreadPool::TaskToken ThreadPool::execute(NonReturnFunc &&backend_task, NonReturnFunc &&main_cb, int prio)
{
TaskToken token;
if (!d_->is_ready) {
LogWarn("need initialize() first");
return token;
}
if (prio < THREAD_POOL_PRIO_MIN)
prio = THREAD_POOL_PRIO_MIN;
else if (prio > THREAD_POOL_PRIO_MAX)
prio = THREAD_POOL_PRIO_MAX;
int level = prio + THREAD_POOL_PRIO_MAX;
{
std::lock_guard<std::mutex> lg(d_->lock);
Task *item = d_->task_pool.alloc();
item->backend_task = std::move(backend_task);
item->main_cb = std::move(main_cb);
item->create_time_point = Clock::now();
item->token = token = d_->undo_tasks_cabinet.alloc(item);
d_->undo_tasks_token.at(level).push_back(token);
//! 如果空闲线程不够分配未认领的任务,且还可以再创建新的线程
if (d_->undo_tasks_cabinet.size() > d_->idle_thread_num) {
if (d_->threads_cabinet.size() < d_->max_thread_num) {
createWorker();
} else {
if (d_->undo_task_peak_num_ < d_->undo_tasks_cabinet.size())
d_->undo_task_peak_num_ = d_->undo_tasks_cabinet.size();
}
}
}
LogDbg("create task %u", token.id());
// 唤醒一个工作线程
d_->cond_var.notify_one();
return token;
}
取消任务
只要是任务还在任务队列,那ThreadPool就支持该任务的取消。如果此任务正在被执行,那就无法取消了。
这里实现十分简单,只要删除任务队列中的任务就行了:
/**
* 返回值如下:
* 0: 取消成功
* 1: 没有找到该任务
* 2: 该任务正在执行
*/
int ThreadPool::cancel(TaskToken token)
{
std::lock_guard<std::mutex> lg(d_->lock);
//! 如果正在执行
if (d_->doing_tasks_token.find(token) != d_->doing_tasks_token.end())
return 2; //! 返回正在执行
//! 从高优先级向低优先级遍历,找出优先级最高的任务
for (size_t i = 0; i < d_->undo_tasks_token.size(); ++i) {
auto &tasks_token = d_->undo_tasks_token.at(i);
if (!tasks_token.empty()) {
auto iter = std::find(tasks_token.begin(), tasks_token.end(), token);
if (iter != tasks_token.end()) {
tasks_token.erase(iter);
d_->task_pool.free(d_->undo_tasks_cabinet.free(token));
return 0;
}
}
}
return 1; //! 返回没有找到
}
资源释放
ThreadPool::cleanup提供资源释放的功能,实现比较简单,涉及的操作如下:
- 清空任务队列中的任务
- 唤醒所有挂起的工作线程
- 等待工作线程退出
总结
- 私有数据成员指针封装(内部结构体指针封装)。在面向对象编程中,它体现了封装和数据隐藏的原则,即将数据成员(这里是Data结构体)声明为私有,并通过一个指向该类型的指针(d_)来管理访问和操作,从而保护数据成员不受外界直接干预,仅通过类提供的接口进行交互。
- 条件变量是实现线程池的核心。