Linux应用框架cpp-tbox之线程池

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
    // 其他成员函数、析构函数等...
}; 

这种写法的好处主要包括以下几点:

  1. 封装性:通过将Data结构体定义为私有成员,外界无法直接访问Data的内部细节,仅能通过类提供的公共接口来间接操作,增强了数据的封装性和安全性。
  2. 控制访问与操作隔离:类的设计者可以控制对Data结构体的直接访问和修改仅限于类内部,避免了外部误操作引起的数据不一致性问题。通过成员函数(如getter/setter)来操作d_,可以添加逻辑校验和控制。
  3. 灵活性与扩展性:将来如果Data结构体需要修改,只需在类内部进行,不会影响到使用该类的外部代码。此外,类可以通过添加新的操作函数来扩展对外接口,而无需更改外部代码对Data的引用方式。
  4. 内存管理:通过指针d_管理Data实例的生命周期,可以在类的构造函数中分配内存,在析构函数中释放,这样可以更好地控制内存的分配和释放,减少内存泄漏的风险。当然,这也要求开发者需要管理好资源,避免悬挂指针和内存泄露。
  5. 性能:直接操作指针相比于对象实例,减少了拷贝的开销(尤其是在大型对象或复杂结构体时),可以提高性能。另外,对于某些特定操作,直接操作指针可能提供更大的灵活性和效率。

创建

通过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提供资源释放的功能,实现比较简单,涉及的操作如下:

  1. 清空任务队列中的任务
  2. 唤醒所有挂起的工作线程
  3. 等待工作线程退出

总结

  1. 私有数据成员指针封装(内部结构体指针封装)。在面向对象编程中,它体现了封装和数据隐藏的原则,即将数据成员(这里是Data结构体)声明为私有,并通过一个指向该类型的指针(d_)来管理访问和操作,从而保护数据成员不受外界直接干预,仅通过类提供的接口进行交互。
  2. 条件变量是实现线程池的核心
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值