C++线程池的简单实现

背景

C++中线程频繁的创建会导致有大量的时间开销,因此我们会引入线程池的概念来解决这个问题。而线程池的主要思想则是先一次性创建多个线程,将这些线程加入一个"池"中,当之后如果有大量任务到来之后,这些线程就会不断处理这些任务,这样就避免我们手动创建大量的线程来应对大量的任务。

数据结构的采用

线程池

为了可以将多个线程存放起来,我们需要采用一个方便将它们存入和遍历的数据结构,因此在这里我们考虑到使用C++的vector容器

任务队列

任务队列主要是用来将需要执行的任务存放的数据结构,我们在这里对到来的任务采用的是先来先服务(FIFO)的处理办法,因此我们在此处用的是队列。

类的设计

当我们开始设计类的时候需要关注主要解决的是什么问题,或者怎么样设计类可以更好地解决问题。因此我将先给出代码再来讲解为什么这么设计;

线程池(ThreaPool)

C++ 复制代码
class ThreadPool{
public:
    ThreadPool(size_t threadnum,size_t quesize);//构造函数
    ~ThreadPool();//析构
    void start();//开启线程池
    void stop();//关闭线程池
    void addTask(Task *);//添加新任务
private:
    size_t _threadNum; //线程池能容纳的线程最大数量
    size_t _queSize;   //任务队列所能装的任务最大数量
    vector<thread> _threads; //存放线程的"池"
    TaskQueue _taskQue;//任务队列(详情看下文)
    bool isExit;//线程池是否结束的标志
    Task * getTask();//获取任务
    void doTask();//执行任务
};
 

上面是一个线程池的类,我们在代码的注释里面给出了设计的函数的解释,但是上面有个TaskQueue类的成员却没有仔细提到,让我们在下文仔细研究这个类。这个上面的线程池主要是初始化了一个线程池,并且提供了开启、关闭线程池的功能,然后提供了添加任务的接口,以此来实现了一个简单的线程池。

任务队列(TaskQueue)

C++ 复制代码
class TaskQueue{
public:
    TaskQueue(size_t quesize);//构造函数
    ~TaskQueue();//析构
    void push(Task *);//将任务添加到队尾
    Task * pop();//取出队头的一个任务
    bool full();//任务队列中的任务是否满了
    bool empty();//任务队列中是否还有任务
    void wakeup();//唤醒所有在wait的线程(配合_flag成员使用,后面会讨论)
private:
    size_t _queSize;//任务队列的大小(所能装载任务的大小)
    queue<Task *> _que;//这是任务的装载体
    mutex _mutex;//互斥锁,确保运行时多个任务执行不会出现数据污染
    condition_variable _notFull;//当任务队列满了之后,后来的任务将会wait这个条件变量
    condition_variable _notEmpty;//当任务队列为空的时候,再想把任务添加进任务队列时就会wait这个条件变量
    bool _flag;//线程池结束的时候需要处理这个标志以防止主线程与子线程速度不一(后面会讨论)
};

上面时关于任务队列类的设计,其中的两个部分是用来解决主线程与子线程速度不一样的问题(这个问题我在刚开始设计线程池时踩到了这个坑,因此我在这里想单独分享一下)。在上面类的设计中有Task类,这个Task类是一个关于任务的类,这个类很简单,我在下面给出代码:

C++ 复制代码
class Task{
public:
    virtual void process();//任务处理函数
    Task();//构造
    virtual ~Task();//析构
};

上面三个类是此次设计简单线程池的主要类了,接下来我们来讲具体怎么去实现他们,首先我们从简单的讲起。

类的实现

Task类的实现

这三个类的实现中最简单的当然是Task类,因为它仅仅只需要提供一个处理任务的函数接口即可(process)

C++ 复制代码
#include "Task.h"
Task::Task(){}
Task::~Task(){}
void Task::process(){}

这么简单即可。

线程池(ThreadPool)

话不多说直接上代码!

C++ 复制代码
#include "ThreadPool.h"
#include "Task.h"
#include <iostream>
#include <chrono>
using std::cout;
using std::endl;
ThreadPool::ThreadPool(size_t threadnum,size_t quesize)//构造函数,一些成员直接使用初始化列表的形式
:_threadNum(threadnum)
,_queSize(quesize)
    ,_threads()
    ,_taskQue(quesize)//对任务队列进行初始化
    ,isExit(false)
{}                                 
ThreadPool::~ThreadPool(){}
void ThreadPool::start(){//开启线程池--定义的thread类型的vector直接按照最大线程数创建线程即可
    for(size_t idx=0;idx<_threadNum;idx++){
        _threads.push_back(thread(&ThreadPool::doTask,this));
    }
}
void ThreadPool::stop(){//关闭线程池,将线程池中的线程一一join来完成线程资源的释放
    while(!_taskQue.empty()){
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
    isExit=true;
    _taskQue.wakeup();
    for(auto &ele : _threads){
        ele.join();
    }
    cout<<"ThreadPoll  end!"<<endl;
}
Task* ThreadPool::getTask(){//从任务队列中获得任务
    return _taskQue.pop();
}
void ThreadPool::doTask(){//获得一个任务,如果此时线程池没有退出且可以获得任务就执行任务;如果没退出但是没有任务进入队列的话,就会陷入wait状态,等待被唤醒;如果退出就直接退出
    while(!isExit){
        Task *p=getTask();
        if(p){
        p->process();
        }
        else{
        cout<<"task is null!"<<endl;
        }
    }
}
void ThreadPool::addTask(Task * p){//将任务放进任务队列,若队列满则会阻塞
    if(p){
        _taskQue.push(p);
    }else
    {
        cout<<"task push error! because task *p is null"<<endl;
    }
}

上述代码主要是对一些功能的实现,重要点其实也在于对于容器和一些函数的使用,各个函数的注释已标注!

任务队列(TaskQueue)

上代码!

C++ 复制代码
#include "TaskQueue.h"
TaskQueue::TaskQueue(size_t quesize)//初始化任务队列
:_queSize(quesize)
    ,_que()
    ,_mutex()
    ,_notFull()
    ,_notEmpty()
    ,_flag(false)
{}
TaskQueue::~TaskQueue(){}
using std::unique_lock;
void TaskQueue::push(Task * p){//将任务放进任务队列,这个地方放任务需要进行加锁不然可能会导致线程崩溃!
    unique_lock<mutex> ul(_mutex);
    while(full()){//当新添Task时,需要判断队列是否满了,如果满了就得阻塞这个线程,等待唤醒
        _notFull.wait(ul);
    }
    _que.push(p);
    _notEmpty.notify_one();
}
Task * TaskQueue::pop(){//将任务取出,这个地方也同样也要加锁!
    unique_lock<mutex> ul(_mutex);
    while(empty() && !_flag){//当需要弹出队列的时候,同样需要判断是否为空,若空则阻塞
        _notEmpty.wait(ul);
    }
    if(!_flag)
    {
        Task *temp=_que.front();
        _que.pop();
        _notFull.notify_one();
        return temp;
    }else{
        return nullptr;
    }
}
bool TaskQueue::full(){//判断队列是否满了
    return _que.size()==_queSize;
}
bool TaskQueue::empty(){//判断队列是否是空的
    return _que.size()==0;
}
void TaskQueue::wakeup(){//唤醒所有在wait的线程
    _flag=true;
    _notEmpty.notify_all();
}

上面这个任务队列的类实现中,需要注意的是在实现 TaskQueue.push/TaskQueue.pop 的时候需要给他们添加互斥锁和条件变量,这个地方的思路其实和经典的生产者--消费者模式一样,需要判断缓存区是否有容量。 然后接下来为了更加直观的体现这个简单线程池是如何运行的,我将通过给出测试代码的形式来演示。

测试代码

C++ 复制代码
#include <ctime>
#include <iostream>
#include <memory>
#include "Task.h"
#include "TaskQueue.h"
#include "ThreadPool.h"
using std::cout;
using std::endl;
using std::unique_ptr;
class MyTask
:public Task
{
public:
    virtual void process(){//处理数据的任务函数
        ::srand(::clock());
        int number=::rand()%100;
        cout<<">>Mytask process the number is "<<number<<endl;
    }
};
void test(){
    unique_ptr<Task> ptask(new MyTask());
    ThreadPool pool(3,8);//线程池中的线程数为3个,任务队列数为8个
    pool.start();
    int cnt=15;
    while(cnt--){//总共要执行15个任务
        pool.addTask(ptask.get());
        cout<<"cnt : "<<cnt<<endl;
    }
    pool.stop();
}   
int main()
{
    test();
    return 0;
}

上面这个测试用例,我们初始化了一个线程数3的线程池,和一个任务数最大为8的任务队列。初始化完成之后就开启线程池--pool.start()(注:当线程池start之后就会直接执行doTask,然后doTask就会调用getTask,但此时我们并没有往里面添加任务,所以会暂时阻塞在这个地方,直到后面有任务添加进来就会被唤醒来执行任务);总共需要执行15个任务。然后依次将这些任务通过pool.addTask()加入到线程池中,然后就会按照函数调用顺序依次调用,当所有任务完成之后,整个程序就会像开始一样继续等待任务的进来来唤醒。或者等待收到pool.stop()命令来结束线程池。

运行流程图

下面是程序运行的流程图:

下面是程序正常运行的输出结果:

初次diy线程池可能踩的坑

Q1:主线程比子线程快

这个问题的主要体现就在于,我上面的例子用的是15个例子,但是当我运行完程序之后发现没有输出15个数据程序就结束了。导致这个现象的主要原因就在于,子线程还在进行15个数据的处理过程中,我的主线程也就是测试用例部分的代码就已经跑完了,并执行了pool.stop()的函数,于是这样就会让程序提前结束。而应对这个问题的最好的办法就是当主线程执行到pool.stop时,在这个函数里面添加一个while()循环:这样做的目的就是如果我想结束线程池的话,我必须得等线程池里的任务都执行完毕,也就是任务队列中的任务为空了我才能够结束线程池。解决这个问题的代码如下:

C++ 复制代码
while(!_taskQue.empty()){
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }

上面这段代码添加在THreadPool类的stop函数中。

Q2:子线程比主线程快

这个问题的主要表现结果就是,当我的程序执行后,我发现我的代码已经正常输出了15个数据,但是程序却没有正常退出。那么当我们遇到这种问题的时候第一反应就是这个程序在某个位置卡死了,而反观我的15个数据已经打印完了,那么会在哪里卡死呢?而当我们仔细思考这个问题并且研究代码之后,会发现子线程进行过快的话,会有任务处于not_empty.wait()状态中。这样的话我们的线程就会一直等待有新的任务进入,导致程序卡死了。而解决这个问题的办法就是我们在需要结束线程池的情况下,唤醒所有可能等待的任务也就是执行一个not_empty.notify_all()。并且设置一个flag位,用来判断我的任务队列被唤醒之后,如果有线程继续想要执行任务的话(因为执行的ThreadPool::doTask函数需要用到ThreadPool::getTask获取任务,而这个ThreadPool::getTask调用的是TaskQueue::pop)会先判断这个flag是否true,如果发现这个flag为true之后(这个情况下此时的队列为空),就不会去not_empty.wait(),这样就可以解决子线程比主线程速度快导致的我执行了stop却没有结束程序的问题.这也就是我在前面所说的wakeup()函数和flag标记位配合使用的目的。那么他们的对应代码如下

C++ 复制代码
//TaskQueue.cpp
void TaskQueue::wakeup(){
    _flag=true;
    _notEmpty.notify_all();
}
Task * TaskQueue::pop(){
    unique_lock<mutex> ul(_mutex);
    while(empty() && !_flag){
        _notEmpty.wait(ul);
    }
    if(!_flag)
    {
        Task *temp=_que.front();
        _que.pop();
        _notFull.notify_one();
        return temp;
    }else{
        return nullptr;
    }
}

而用上述解决办法之后,会发现测试结果最后会输出几行task is null!,这个的行数就与我们的线程池能存储的最大线程数有关。

结尾

上面就是我对简单线程池实现的思路,里面可能有些不够精辟或者有问题的地方,希望看到的读者可以留下你们宝贵的建议,让我一起共同进步!接下来我附上我的完整代码!

C++ 复制代码
//ThreaPoo.h-----------------------------------
#ifndef __THREADPOOL_H
#define __THREADPOOL_H
#include <thread>
#include <vector>
#include "TaskQueue.h"
#include "Task.h"
using std::vector;
using std::thread;
class ThreadPool{
public:
    ThreadPool(size_t threadnum,size_t quesize);
    ~ThreadPool();
    void start();
    void stop();
    void addTask(Task *);
private:
    size_t _threadNum;
    size_t _queSize;
    vector<thread> _threads;
    TaskQueue _taskQue;
    bool isExit;
    Task * getTask();
    void doTask();
};
#endif
//ThreadPool.cpp---------------------------------------------------------
#include "ThreadPool.h"
#include "Task.h"
#include <iostream>
#include <chrono>
using std::cout;
using std::endl;
ThreadPool::ThreadPool(size_t threadnum,size_t quesize)
:_threadNum(threadnum)
,_queSize(quesize)
    ,_threads()
    ,_taskQue(quesize)
    ,isExit(false)
{}
ThreadPool::~ThreadPool(){}
void ThreadPool::start(){
    for(size_t idx=0;idx<_threadNum;idx++){
        _threads.push_back(thread(&ThreadPool::doTask,this));
    }
}
void ThreadPool::stop(){
    while(!_taskQue.empty()){
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
    isExit=true;
    _taskQue.wakeup();
    for(auto &ele : _threads){
        ele.join();
    }
    cout<<"ThreadPoll  end!"<<endl;
}
Task* ThreadPool::getTask(){
    return _taskQue.pop();
}
void ThreadPool::doTask(){
    while(!isExit){
        Task *p=getTask();
        if(p){
        p->process();
        }
        else{
        cout<<"task is null!"<<endl;
        }
    }
}
void ThreadPool::addTask(Task * p){
    if(p){
        _taskQue.push(p);
    }else
    {
        cout<<"task push error! because task *p is null"<<endl;
    }
}
//TaskQueue.h---------------------------------------------
#ifndef __TASKQUEUE_H 
#define __TASKQUEUE_H
#include <queue>
#include <mutex>
#include <thread>
#include <condition_variable>
#include "Task.h"
using std::queue;
using std::size_t;
using std::mutex;
using std::condition_variable;
class TaskQueue{
public:
    TaskQueue(size_t quesize);
    ~TaskQueue();
    void push(Task *);
    Task * pop();
    bool full();
    bool empty();
    void wakeup();
private:
    size_t _queSize;
    queue<Task *> _que;
    mutex _mutex;
    condition_variable _notFull;
    condition_variable _notEmpty;
    bool _flag;
};
#endif
//TaskQueue.cpp-------------------------------------------------
#include "TaskQueue.h"
TaskQueue::TaskQueue(size_t quesize)
:_queSize(quesize)
    ,_que()
    ,_mutex()
    ,_notFull()
    ,_notEmpty()
    ,_flag(false)
{}
TaskQueue::~TaskQueue(){}
using std::unique_lock;
void TaskQueue::push(Task * p){
    unique_lock<mutex> ul(_mutex);
    while(full()){
        _notFull.wait(ul);
    }
    _que.push(p);
    _notEmpty.notify_one();
}
Task * TaskQueue::pop(){
    unique_lock<mutex> ul(_mutex);
    while(empty() && !_flag){
        _notEmpty.wait(ul);
    }
    if(!_flag)
    {
        Task *temp=_que.front();
        _que.pop();
        _notFull.notify_one();
        return temp;
    }else{
        return nullptr;
    }
}
bool TaskQueue::full(){
    return _que.size()==_queSize;
}
bool TaskQueue::empty(){
    return _que.size()==0;
}
void TaskQueue::wakeup(){
    _flag=true;
    _notEmpty.notify_all();
}

//Task.h-----------------------------------
#ifndef __TASK_H 
#define __TASK_H
class Task{
public:
    virtual void process();
    Task();
    virtual ~Task();
};
#endif
//Task.cpp
#include "Task.h"
Task::Task(){}
Task::~Task(){}
void Task::process(){}

//TestThreadPool.cpp
#include <ctime>
#include <iostream>
#include <memory>
#include "Task.h"
#include "TaskQueue.h"
#include "ThreadPool.h"
using std::cout;
using std::endl;
using std::unique_ptr;
class MyTask
:public Task
{
public:
    virtual void process(){
        ::srand(::clock());
        int number=::rand()%100;
        cout<<">>Mytask process the number is "<<number<<endl;
    }
};
void test(){
    unique_ptr<Task> ptask(new MyTask());
    ThreadPool pool(6,8);
    pool.start();
    int cnt=15;
    while(cnt--){
        pool.addTask(ptask.get());
        cout<<"cnt : "<<cnt<<endl;
    }
    pool.stop();
}   
int main()
{
    test();
    return 0;
}
相关推荐
R-G-B1 小时前
【28】MFC入门到精通——MFC串口 Combobox 控件实现串口号
c++·mfc·mfc串口控件·combobox 控件·mfc串口参数控件实现
L_B__1 小时前
C++ shared_mutex 探索:从 futex 逐渐深入到 glibc pthread_rwlock
linux·c++
SunkingYang1 小时前
MFC/C++语言怎么比较CString类型 第一个字符
c++·mfc·方法·cstring·比较·第一个字符
R-G-B1 小时前
【24】MFC入门到精通——MFC在静态框中 更改字体、颜色、大小 、背景
c++·mfc·mfc在静态框中更改字体·mfc静态框更改字体颜色·mfc静态框更改字体大小·mfc静态框更改字体背景
鸿儒5171 小时前
C++ Qt插件开发样例
开发语言·c++·qt
啊森要自信2 小时前
【Linux 学习指南】网络编程基础:从 IP、端口到 Socket 与 TCP/UDP 协议详解
linux·运维·服务器·网络·c++
刚入坑的新人编程3 小时前
暑期算法训练.2
c++·算法
hardStudy_h3 小时前
C++——模版(函数模版和类模版)
开发语言·c++
有趣的我3 小时前
30 天自制 C++ 服务器--Day3
服务器·c++
豆浩宇4 小时前
Halcon双相机单标定板标定实现拼图
c++·人工智能·目标检测·机器学习·计算机视觉