C++11 编写可复用多线程任务池 (开源OEasyPool)

本文详细介绍了如何使用C++11实现一个线程池,包括任务基类`Task`、任务队列`TaskQueue`和线程池`ThreadPool`的实现。线程池配置、任务的添加、删除和监控等方面都进行了讲解,同时讨论了智能指针、原子类型、无序Map和锁机制等技术的应用。示例代码展示了线程池的初始化、任务调度和资源释放过程。
摘要由CSDN通过智能技术生成

引言

涉及内容

  • 智能指针

  • 原子类型

  • 无序Map

  • 线程

  • 条件变量

  • 锁机制

大雄的线程池

  原来线程池是这么来的,我也很惊讶。是大雄的愚蠢,才让我们见识到这么厉害的宝贝。

  有人说,大雄拿到的明明是自动鱼池,我们的是线程池,压根是两个东西。
  
  其实,这也多亏了大雄,因为大雄最近也在学编程。

  至于大雄学编程的故事,我们下回再讲。^_^.

  大雄不甘寂寞写了一份线程池分享给大家 [点击免费下载]

结语

  线程池是为了避免创建和回收,以减少不必要的资源开销,维护特定的线程资源,在需要的时候使用,不需要的时候等待需要,长时间处于闲置的线程,则主动释放。这样的运行机制,在请求频繁的系统中广为应用。更高级的用法还有多线程池的方法。

使用介绍

接下来,我将以逻辑顺序的方式讲解线程池的制作过程。(逻辑方式便于理解)若没有源码的朋友,可以翻到上面,下载源码后,跟着文章一起阅读。

类的功能

  • Task (任务基类)

    该类主要实现一个任务类
    virtual int doWork() = 0;

  • TaskQueue (任务队列)

    该类主要针对任务的存储、删除、撤回等状态做管理

  • ThreadPool (线程池)

    整个线程池的核心业务处理类

Main函数

我们来讲解第一个示例Demo test_base

  • ThreadPoolDemo.cpp

  • #include <time.h>
    
    #include <iostream>
    
    #include <memory>
    
    #include "ThreadPool.h"
    
    #include "../test/TaskTest.h"
    
    using namespace std;
    
    int main(void)
    {
        OEThreadPool::ThreadPoolConfig threadPoolConfig;
        threadPoolConfig.nMaxThreadsNum = 100;
        threadPoolConfig.nMinThreadsNum = 5;
        threadPoolConfig.dbTaskAddThreadRate = 3;
        threadPoolConfig.dbTaskSubThreadRate = 0.5;
        clock_t start = clock();
        {
    
            std::shared_ptr<OEThreadPool> threadPool(new OEThreadPool);
            threadPool->init(threadPoolConfig);
    
            int nID = 0;
            while (true)
            {
                std::shared_ptr<OETaskTest> request = std::shared_ptr<OETaskTest>(new OETaskTest());
    
                threadPool->addTask(request);
                if (request->getID() == 101000) {
                    break;
                }
            }
    
            threadPool->release();
        }
        clock_t finish = clock();
        std::cout << "duration:" << finish - start << "ms"<< std::endl;
        getchar();
        return 0;
    }

    这里主要做了一些线程池的运行测试工作,我们从这个Demo,可以了解到对这个线程池基本的操作。

    1. 线程池的初始化
    2. 创建任务类
    3. 添加任务到线程池当中
    4. 线程池异步处理
    5. 清理线程池资源

配置

关于线程池的配置,我们也留出了接口。
通过 OEThreadPool 类中的 tagThreadPoolConfig 结构体与 init 函数,进行线程池的资源配置。

代码中已经有了比较详尽的注释,但是在这里我还是想贴出来再强调一遍,虽然他很简单,但是看起来非常重要。

/// 线程池配置参数
typedef struct tagThreadPoolConfig {
    int nMaxThreadsNum;         /// 最大线程数量
    int nMinThreadsNum;         /// 最小线程数量
    double dbTaskAddThreadRate;   /// 增 最大线程任务比 (任务数量与线程数量,什么比例的时候才加)
    double dbTaskSubThreadRate;   /// 减 最小线程任务比 (任务数量与线程数量,什么比例的时候才减)
} ThreadPoolConfig;

线程池在没有合理的init之前,是不会开展工作的。

扩展

关于任务线程池的扩展,我们已经提供了很好的解决方案。
例如继承 Task 类,来完成线程任务的扩展。

例如案例中 TaskTest.h 中使用的那样,将主要的业务逻辑放在 doWork 中即可。

讲解

从关联的角度讲解各个类的实现

OETask –> OETaskQueue –> OEThreadPool

结构图

1

主要从任务基类开始做延伸扩展。

2

点击查看大图

任务抽象基类

文件名:Task.h

#ifndef __OETASK_H__
#define __OETASK_H__

#include <atomic>

// 任务基类
class OETask
{

protected:

    // 任务的唯一标识
    int id_;

private:
    static int nRequestID_;
    // 任务取消状态
    std::atomic<bool>  bIsCancelRequired_;

public:
    OETask() :id_(nRequestID_++), bIsCancelRequired_(false) {};
    virtual ~OETask() {};

public:
    // 任务类虚接口,继承这个类的必须要实现这个接口
    virtual int doWork() = 0;

    // 任务已取消回调
    virtual int onCanceled(){ return 1; }
    // 任务已完成
    virtual int onCompleted(int){ return 1; }

    // 获取任务ID
    int getID(){ return id_; }
    // 设置ID
    void setID(int nID){ id_ = nID; }
    // 获取任务取消状态
    bool isCancelRequired(){ return bIsCancelRequired_; }
    // 设置任务取消状态
    void setCancelRequired(){ bIsCancelRequired_ = true; }

};

__declspec(selectany) int OETask::nRequestID_ = 100000;
#endif // __OETASK_H__

这是一个抽象的任务基类, 他的声明是如此简单,是不是觉得有些鸡肋? 其实不是的,你可以根据这个基类,做一些自己的扩展。

例如,我做了以下扩展,来帮助我查询任务的执行状态,有效的异步监控任务。

class SKTask :
    public OETask
{
public:

    SKTask() :OETask() {
        CommonData::setTaskStatus(id_, MODULE_TASK_CREATE);
    }

    virtual ~SKTask() {
    }

    virtual int doWork() {
        CommonData::setTaskStatus(id_, MODULE_TASK_PROCESS);
        return 0;
    }

    virtual int onCanceled(void) {
        CommonData::setTaskStatus(id_, MODULE_TASK_CANCEL);
        return 0;
    }
    // 任务已完成
    virtual int onCompleted(int code) {
        CommonData::setTaskStatus(id_, code);
        return 0;
    }
};

这里有必要着重讲一下的是 onCompleted 函数,这个函数的参数是doWork的返回值。

当然 你也可以做很多你自己的扩展,例如添加几个成员函数,再或者使用一下C++11 的 bind 函数,是的这个函数非常棒,至少我很喜欢。如果你觉得这个函数很面生,我想你有必要好好的了解下他。

任务队列

文件名:taskqueue.h

#ifndef __OETASKQUEUE_H__
#define __OETASKQUEUE_H__

#include <deque>
#include <mutex>
#include <condition_variable>
#include <unordered_map>
#include <memory>

#include "Task.h"

/// 任务队列类
class OETaskQueue
{
public:
    OETaskQueue();
    ~OETaskQueue();

private:
    /// 就绪任务队列
    std::deque<std::shared_ptr<OETask> > queue_;
    /// 运行任务map
    std::unordered_map<int, std::shared_ptr<OETask>> mapDoingTask_;
    /// 互斥量
    std::mutex mutex_;
    /// 条件变量
    std::condition_variable conditPut_;

public:
    /**
    * @brief :向队列的末尾插入任务
    * @param :task 任务类
    */
    void put_back(std::shared_ptr<OETask> task);
    /**
    * @brief :向队列的头部插入任务
    * @param :task 任务类
    */
    void put_front(std::shared_ptr<OETask> task);
    /**
    * @brief :获取队首(并将任务加到运行任务列表中)
    * @return:任务类
    */
    std::shared_ptr<OETask> get(void);
    /**
    * @brief :获取整个双向链表的大小
    * @return:大小
    */
    size_t size(void);

    /**
    * @brief :释放队列
    */
    void release(void);
    /**
    * @brief :删除任务(从就绪队列删除,如果就绪队列没有,则看执行队列有没有,有的话置下取消状态位)
    * @param :nID 任务的编号
    * @return:成功返回0 失败返回非0
    */
    int deleteTask(int nID);
    /**
    * @brief :删除所有任务
    * @return:成功返回0 失败返回非0
    */
    int deleteAllTasks(void);
    /**
    * @brief :任务完成回调(从运行列表中删除指定任务)
    * @param :nID 任务的编号
    * @return:成功返回0 失败返回非0
    */
    int onTaskFinished(int nID);
    /**
    * @brief :判断任务是否执行完毕
    * @param :nID 任务的编号
    * @return:任务类
    */
    std::shared_ptr<OETask> isTaskProcessed(int nId);

    /**
    * @brief :等待有任务到达(带超时:超时自动唤醒)
    * @param :millsec 超时时间(毫秒)
    * @return:成功返回true 失败返回false
    */
    bool wait(std::chrono::milliseconds millsec);

};

#endif // __OETASKQUEUE_H__

这个队列针对任务的存储、删除、撤回等状态做了一系列的管理。使之有序的被执行(我反正是按顺序派发的任务,至于异步执行而导致的时间片的占用效率问题,我可管不了

  • 智能锁

    这里对异步任务的提取工作,一定要注意,所以我使用了 mutex 做了一些线程安全方面的保护,对于程序而言, mutex 还是显得格格不入,我利用 std::unique_lock<std::mutex> 让这个 mutex 显得有些生机。

    如果你经常使用多线程编程,而且线程管理范围达几十个,我想你会喜欢 C++11 的,更准确的说,你会喜欢 C++11 的智能锁。

线程池

文件名:threadpool.h

#ifndef __OETHREADPOOL_H__
#define __OETHREADPOOL_H__

#include "Task.h"
#include "TaskQueue.h"

/// 任务管理类
class OEThreadPool
{
public:
    /// 线程池配置参数
    typedef struct tagThreadPoolConfig {
        int nMaxThreadsNum;         /// 最大线程数量
        int nMinThreadsNum;         /// 最小线程数量
        double dbTaskAddThreadRate;   /// 增 最大线程任务比 (任务数量与线程数量,什么比例的时候才加)
        double dbTaskSubThreadRate;   /// 减 最小线程任务比 (任务数量与线程数量,什么比例的时候才减)
    } ThreadPoolConfig;

private:
    /// 任务队列
    OETaskQueue taskQueue_;

    /// 线程池配置(如果最小线程数量为1,则表示需要一个常驻的处理线程)
    ThreadPoolConfig threadPoolConfig_;
    /// 线程池是否被要求结束
    std::atomic<bool> atcWorking_;
    /// 当前线程个数
    std::atomic<int>  atcCurTotalThrNum_;
    /// 互斥量
    std::mutex mutex_;

public:
    OEThreadPool(void);
    ~OEThreadPool(void);

    /**
    * @brief :线程池资源配置初始化
    * @param :config 初始化的配置信息
    * @return:0 执行成功  非0 执行失败
    */
    int init(const ThreadPoolConfig& config);
    /**
    * @brief :释放资源(释放线程池、释放任务队列)
    * @return:true 执行成功  false 执行失败
    */
    bool release(void);

    /**
    * @brief :添加任务
    * @param :taskptr 任务类
    * @param :priority 是否有限处理 true:优先处理
    * @return:0 执行成功  非0 执行失败
    */
    int addTask(std::shared_ptr<OETask> taskptr, bool priority = false);

    /**
    * @brief :删除任务(从就绪队列删除,如果就绪队列没有,则看执行队列有没有,有的话置下取消状态位)
    * @param :nID 任务编号
    * @return:0 执行成功  非0 执行失败
    */
    int deleteTask(int nID);

    /**
    * @brief :删除所有任务
    * @return:0 执行成功  非0 执行失败
    */
    inline int deleteAllTasks(void);
    /**
    * @brief :判断任务是否执行完毕
    * @param :nID 任务编号
    * @return:执行完毕,执行完返回null,否则返回任务指针
    */
    inline std::shared_ptr<OETask> isTaskProcessed(int nId);

private:
    /**
    * @brief :获取当前线程任务比
    * @return:线程任务比
    */
    double getThreadTaskRate(void);
    /**
    * @brief :当前线程是否需要结束
    * @return:true:可以结束 false:不可以结束
    * @note  :已考虑到最小线程数量
    */
    bool shouldEnd(void);
    /**
    * @brief :添加指定数量的处理线程 
    * @param :nThreadsNum 添加的线程数量
    * @return:0 执行成功  非0 执行失败
    */
    int addProThreads(int nThreadsNum);
    /**
    * @brief :释放线程池
    * @return:true 执行成功  false 执行失败
    */
    bool releaseThreadPool(void);
    /**
    * @brief :任务处理线程函数
    */
    void taskProcThread(void);

};

extern OEThreadPool SystemThreadPool;

#endif // __OETHREADPOOL_H__

总结

这里的注释都是遵循 doxygen 的文档系统的注释规则,可以使用 doxygen 一键生成,可供查阅的文档。

  • 我们的收获

    1. 学习并制作了一个可复用的线程池
    2. 初步运用C++11的线程锁、条件变量、无序Map、智能指针等
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值