C++线程池

一、知识储备

1.与线程相关知识点

无论是单核还是多核IO密集型更适合设计多线程程序,不会浪费资源,在多核里CPU密集型适合设计多线程程序。

2.与C++相关知识点

2.1Any类型

cpp 复制代码
//Any类型:可以接收任意数据的类型
class Any
{
public:
    template<typename T>
    Any(T data) :base_(new Derive<T>(data))
    {}
private:
    //基类类型
    class Base
    {
    public:
        virtual ~Base() = default;
    };

    //派生类类型
    template<typename T>
    class Derive : public Base
    {
    public:
        Derive(T data):data_(data)
        {}
    private:
        T data_;
    };
private:
    //定义一个基类的指针
    std::unique_ptr<Base> base_;
};

2.2 智能指针

1、std::make_unique

(1)使用std::make_unique的原因

**异常安全风险:**如果在new操作和unique_ptr构造之间发生异常(例如在计算构造函数参数时),那么new分配的内存可能无法被unique_ptr接管,从而发生内存泄漏。

make_unique可以解决这个风险,make_unique在内部一次性完成内存分配和对象构造。如果构造函数参数的计算过程中抛出异常,或者构造函数本身抛出异常,因为此时还没有返回unique_ptr,所以不会有内存泄漏(分配的内存会被自动释放)。

(2)关键特性

**所有权独占:**std::make unique 创建的 std::unique ptr 拥有对所创建对象的独占所有权。这意味着同一时刻只有一个unique ptr可以指向该对象。所有权可以通过std::move 转移。

**自动内存管理:**当unique_ptr被销毁(例如离开作用域)时,它所管理的对象会被自动删除(调用其析构函数并释放内存)

不支持自定义删除器: std::make_unique 不支持指定自定义删除器。如果需要自定义删除器,必须直接使用 std::unique_ptr 的构造函数,例如 std::unique_ptr<T,D> ptr(new T,custom_deleter);o

**不能用于std::shared_ptr:**std::make unique 只创建 std::unique ptr。要创建std::shared ptr,应使用 std::make shared。

二、需要注意的点

1、创建线程列表时为了避免手动释放创建的线程需要使用unique ptr

2、线程对象被取消时防止线程函数同时被取消,需要设置分离线程

3、在设置提交任务返回机制的时候,不能用task->getResult()要用Result(task),因为线程执行完task,task对象就被析构掉了

三、主要代码结构

1.数据结构

cpp 复制代码
enum class PoolMode //线程池工作模式
{
    MODE_FIXED,//固定数量的线程
    MODE_CACHED//线程数量可动态增长
};

//任务类
class Task
{
public:
    //用户自定义任意任务类型,从Task继承,重写run方法,实现任务自定义
    virtual void run()=0;
private:

};

class Thread
{
public:
    //线程函数对象类型
    using ThreadFunc = std::function<void()>;
    //线程构造函数
    Thread(ThreadFunc func);
    //析构函数
    ~Thread();
    //启动线程
    void start();
private:
    ThreadFunc func_;
};

//线程池类
class ThreadPool
{
public:
    //线程池构造
    ThreadPool();
    //析构函数
    ~ThreadPool();
    //设置线程池的工作模式
    void setMode(PoolMode mode);
    //设置task任务队列上限阈值
    void settaskQueMaxThreshHold(int threshhold);
    //给线程池提交任务
    void submitTask(std::shared_ptr<Task> sp);
    //开启线程池
    void start(int initThreadSize = 4);
    
    ThreadPool(const ThreadPool&)=delete;
    ThreadPool &operator=(const ThreadPool&)=delete;
private:
    //定义线程函数
    void threadFunc();

private:
    /*
    如果new一个Thread需要delete,避免手动delete,
    将vector<Thread*>改为vector<std::unique_ptr<Thread>>,
    std::make_unique 是在C++14中引入的一个功能。如果你使用的是C++11标准,
    则需要升级到C++14或更高版本。
    */
    std::vector<std::unique_ptr<Thread>>threads_;//线程列表
    int initThreadSize_;//线程初始数量
    std::queue<std::shared_ptr<Task>>taskQue_;//任务队列
    std::atomic_int taskSize_;//任务初始数量
    long unsigned int taskQueMaxThreshHold_;//任务队列数量上限阈值
    std::mutex taskQueMtx_;//保证任务队列的线程安全
    
    std::condition_variable notFull_;//表示任务队列的不满
    std::condition_variable notEmpty_;//表示任务队列的不空

    pthread_mutex_t thread_mutex = PTHREAD_MUTEX_INITIALIZER;

    PoolMode poolMode_;//当前线程池的工作模式
};

2.函数

1、提交任务

cpp 复制代码
//给线程池提交任务 用户调用该接口,传入任务对象,生产任务
void ThreadPool::submitTask(std::shared_ptr<Task> sp)
{
    //获取锁
    std::unique_lock<std::mutex> lock(taskQueMtx_);
    //线程的通信 等待任务队列有空余
    //用户提交任务,最长不能阻塞超过1s,否则判断提交任务失败,返回 
    if(!notFull_.wait_for(lock,std::chrono::seconds(1),
        [&]()->bool{if(taskQue_.size() == taskQueMaxThreshHold_) {std::cout<<"任务队列已满需要等待..."<<std::endl;} return taskQue_.size() < taskQueMaxThreshHold_;}))
    {
        //表示notFull_等待1s,条件依然没有满足
        std::cerr<<"task queue is full,submit task fail."<<std::endl;
        return;
    }
    //如果有空余,把任务放入任务队列中
    taskQue_.emplace(sp);
    taskSize_++;
    std::cout<<"任务提交成功!"<<std::endl;
    notEmpty_.notify_all();
    
}

2、消费任务

cpp 复制代码
//定义线程函数 线程池的所有线程从任务队列里面消费任务
void ThreadPool::threadFunc()
{
    for (;;)
    {
        std:: shared_ptr<Task> task;
        {
            //先获取锁
            std::unique_lock<std::mutex>lock(taskQueMtx_);
            std::cout<<std::this_thread::get_id()<<"尝试获取任务..."<<std::endl;
            //等待notEmpty条件
            notEmpty_.wait(lock,[&]()->bool{return taskQue_.size() > 0;});
            std::cout<<std::this_thread::get_id()<<"任务获取成功!开始执行任务..."<<std::endl;
            //从任务队列中取一个任务出来
            task = taskQue_.front();
            taskQue_.pop();
            taskSize_--;

            //如果依然有剩余任务,继续通知其它的线程执行任务
            if (taskQue_.size() > 0)
            {
                notEmpty_.notify_all();
            }

            //取出一个任务,进行通知
            notFull_.notify_all();
        }//增加作用域,当出了作用域,锁就会释放掉
        

        //当前线程负责执行这个任务
        if(task != nullptr)
        {
            task->run();
        }
    }
    

}

3、开启线程池

cpp 复制代码
//开启线程池
void ThreadPool::start(int initThreadSize)
{
    //记录初始线程数量
    initThreadSize_=initThreadSize;
    //创建线程对象
    for (int i = 0; i < initThreadSize_; i++)
    {   //创建thread线程对象的时候,把线程函数给到thred线程对象
        auto ptr = std::make_unique<Thread>(std::bind(&ThreadPool::threadFunc,this));
        threads_.emplace_back(std::move(ptr));//
    }
    //启动所有线程
    for (int i = 0; i < initThreadSize_; i++)
    {
        threads_[i]->start();
    }
}

4、如何使用线程池