一个Windows下线程池的实现(c++) -------笔记

  本篇的代码来自于 一个Windows下线程池的实现(c++),同时,由于我的开发环境是clion+cmake,不是用的vs,所以也贴一下源码地址:这里

  原文中工作原理图已经很明白的介绍了这个线程池的实现架构,这里为了我学习的需要,从代码角度分析这个小例子。

   代码结构方面,我将整个demo分为了两块,线程管理包,以及 任务包。   二者是以动态库的形式进行调用,也是为了熟悉cmake的使用。 

   thread包中,核心类ThreadPool的api如下,以代码块的形式进行解读(threadPool此处实际上更像一个命名空间):

typedef int(*TaskFun) (PVOID param);
typedef void(*TaskCallbackFun) (int result);

   首先,定义了两个 函数指针,这是以前没有接触过的新鲜玩意,没想到函数还能当作变量一样使用,这是大大的拓宽了我的眼界,同时让我想到了java8的 新特性里面就有传递参数的好像。   这两个函数的作用: 一个为 线程的任务函数,它的参数为无状态指针,返回值为 int; 另一个为 任务回调函数,以状态码为参数,没有返回值。 

//threadpool的线程内部类
class Thread{
    public:
        Thread(ThreadPool *threadPool);
        ~Thread();

        BOOL  isBusy(); //是否有任务在执行
        void executeTask(TaskFun task,PVOID param,TaskCallbackFun taskCallback); //执行任务
    private:
        ThreadPool *threadPool;  //所属线程池
        BOOL  busy; //是否有任务在执行
        BOOL  exit;//是否退出
        HANDLE  thread; //线程句柄
        TaskFun  task;  //要执行的任务
        PVOID param; //任务参数
        TaskCallbackFun  taskCb; //回调任务
        static unsigned int __stdcall ThreadProc(PVOID pM);  //线程函数
    };

   这里要注意的是,此处的线程为 系统线程的抽象,包括线程状态的维护等等,这个抽象的原因在于,我们可以复用。因为原生线程它的某种状态与实际状态时一致的,这就说明,它相应状态的改变,必然会引起相应的操作,当线程状态变更频繁时,效率会打折扣

//IOCP的通知种类
    enum WAIT_OPERATION_TYPE{
        GET_TASK, //获取到任务
        EXIT //退出
    };

    这是定义在threadpool的内部枚举,用于线程间通信的消息类型。 

 //threadpool 的内部类,待执行的任务
class WaitTask{
    public:
        WaitTask(TaskFun task,PVOID param,TaskCallbackFun taskCb,BOOL bLong);
        ~WaitTask();

        TaskFun  task;//要执行的任务
        PVOID param;//任务参数
        TaskCallbackFun taskCb;// 回调的任务
        BOOL bLong; //是否时长任务
    };

     这个waitTask类比到java中,就是个典型的PO类,属性集。

//线程临界区锁
class CriticalSectionLock{
    private:
        CRITICAL_SECTION  cs;//临界区
    public:
        CriticalSectionLock();
        ~CriticalSectionLock();

        void Lock();
        void UnLock();
    };

   线程临近区,用于多线程环境下,保证线程安全。 

public:
    ThreadPool(size_t minNumOfThread =2,size_t maxNumOfThread =10);
    ~ThreadPool();

    BOOL QueueTaskItem(TaskFun task,PVOID param,TaskCallbackFun taskCb = NULL,BOOL longFun = FALSE); //任务入队

private:
    size_t  getCurNumOfThread(); //获取线程池中的当前线程数
    size_t getMaxNumOfThread(); //获取线程池中的最大线程数
    void setMaxNumOfThread(size_t size); //设置线程池中的最大线程数
    size_t getMinNumOfThread(); //获取线程池中的最小线程数
    void setMinNumOfThread(size_t size); //设置线程池中的最小线程数

    size_t getIdleThreadNum(); //或许线程池中的空闲线程数
    size_t getBusyThreadNum(); //获取线程池中的运行线程数
    void createIdleThread(size_t size); //创建空闲线程
    void deleteIdleThread(size_t size); //删除空闲线程
    Thread *getIdleThread(); //或许空闲线程
    void moveBusyThreadToIdleList(Thread *busyThread); //忙碌线程加入空闲队列
    void moveThreadToBusyList(Thread *thread); //线程加入忙碌列表
    void getTaskExecute(); //从任务队列中取任务执行

    WaitTask *getTask(); //从任务队列中取任务

    CriticalSectionLock idleThreadLock; //空闲线程列表锁
    std::list<Thread *> idleThreadList; //空闲线程列表
    CriticalSectionLock busyThreadLock; //忙碌线程列表锁
    std::list<Thread *> busyThreadList; //忙碌线程列表


    CriticalSectionLock waitTaskLock; //任务锁
    std::list<WaitTask*> waitTaskList; //任务列表

    HANDLE dispatchThread; //分发任务线程
    HANDLE stopEvent; //通知线程退出的事件
    HANDLE completionPort; //完成端口

    size_t maxNumOfThread; //线程池中最大的线程数
    size_t minNumOfThread; //线程池中最小的线程数
    size_t numOfLongFun; //线程池中时常 线程数

   上面则是threadPool的一些核心属性及接口,它的核心有两点,第一,管理线程(注,这个是抽象的线程。); 第二,管理任务。api过完了,接着将关键的实现部分给看一看。 

   

ThreadPool::ThreadPool(size_t minNumOfThread,size_t maxNumOfThread){
    if(minNumOfThread<2)
        this->minNumOfThread =2;
    else
        this->minNumOfThread=minNumOfThread;

    if(maxNumOfThread<this->minNumOfThread*2)
        this->maxNumOfThread = this->minNumOfThread*2;
    else
        this->maxNumOfThread = maxNumOfThread;

    //停止事件
    stopEvent = CreateEvent(NULL,TRUE,FALSE,NULL);
    //实例化完成端口,分配内存IO
    completionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,1);

    idleThreadList.clear();
    createIdleThread(this->minNumOfThread);

    busyThreadList.clear();

    //实例化分发线程
    dispatchThread = (HANDLE) _beginthreadex(0,0,GetTaskThreadProc,this,0,0);
    numOfLongFun =0;
}

    threadPool在实例化时,会传入最小线程数,和 最大线程数 这两个参数,方法内部会对最小线程数进行限度,不得小于2个。最大线程数低于最小线程数的2倍,则最大线程数为 当前最小线程数的两倍。 

    然后通过windows的API分配资源,包括 停止事件句柄完成端口句柄。之后,根据最小线程数,创建空闲线程,并加入空闲线程队列。 同时为了确保安全性,清空活跃线程队列。

    最后,开启一个分发任务的线程,这个线程用于获得任务,然后将任务派发给空闲线程。

void ThreadPool::createIdleThread(size_t size) {
    idleThreadLock.Lock();
    for(size_t i=0;i<size;i++){
        idleThreadList.push_back(new Thread(this));
    }
    idleThreadLock.UnLock();
}

   创建线程时,是以线程安全(临界区)的形式,将新实例化的线程加入 空闲线程队列。 

ThreadPool::Thread::Thread(ThreadPool *threadPool):busy(FALSE),thread(INVALID_HANDLE_VALUE),task(NULL),taskCb(NULL),exit(FALSE),threadPool(threadPool)
{
    thread = (HANDLE)_beginthreadex(0, 0, ThreadProc, this, CREATE_SUSPENDED, 0);
}

   而实例化抽象线程的时候,方法内部会 在操作系统中 实际上开启一个线程。  不过有几个注意的点:

       1.新开的线程状态是: CREATE_SUSPENDED,意味着该线程被系统分配后,并不会立即参与调度执行,而是挂起在线程队列中。 

       2.抽象的线程在实例化时,均会反向绑定到一个ThreadPool。由于本程序中,是以内部类的形式设计的,所以它实际上绑定的就是外部的ThreadPool对象。 

       3.新开的线程,以当前抽象线程为参数,进行传递。 

   接着我们看下,新开线程的任务是一个怎样的逻辑:

unsigned int ThreadPool::Thread::ThreadProc(PVOID pM) {
    Thread *pThread = (Thread*)pM;
    while (true){
        if(pThread->exit)break; //线程退出
        //首次检查
        if(pThread->task==NULL&&pThread->taskCb==NULL){
            pThread->busy = FALSE;
            pThread->threadPool->moveBusyThreadToIdleList(pThread);
            SuspendThread(pThread->thread);
            continue;
        }
        //构造方法
        int result =pThread->task(pThread->param);
        if(pThread->taskCb)pThread->taskCb(result);

        WaitTask *waitTask = pThread->threadPool->getTask();
        //取到了任务就继续执行
        if(waitTask!=NULL){
            pThread->task = waitTask->task;
            pThread->taskCb = waitTask->taskCb;
            delete waitTask;
            continue;
        } else{
            pThread->task =NULL;
            pThread->param = NULL;
            pThread->taskCb =NULL;
            pThread->busy = FALSE;
            pThread->threadPool->moveBusyThreadToIdleList(pThread);
            SuspendThread(pThread->thread);
        }
    }
    return 0;
}

    从代码中,我们可以知晓这样的一些信息:

      1.当前线程的执行逻辑首先是一个 无限循环的路径;

      2.每条路径中,会根据传入的抽象线程的状态,做出相应的动作。

          如抽象退出状态被激活,则退出该无限循环,继而这个线程也会退出。

          然后判断 传入的抽象线程是否有 任务以及 任务对调函数,如二者均为空,则修改当前的抽象线程的状态为空闲,并从活跃线程队列 移至 空闲线程队列。   然后挂起当前抽象线程的 实际线程句柄(而不是回收,注意)。 

          以上检查均通过后,通过前面定义的 函数指针,后加括号,进行函数执行。根据执行的任务状态码,有任务回调函数则执行任务回调,同时我也认为,这是整个设计流程之所以能够运行的最根本所在。 这是另一个颠覆我以前认知的地方,在以前学习js的时候,也学过自执行函数,闭包等等,也有介绍收 "()" 为函数触发符号,本以为那个js的个性,没想到c系语言也是这样。后面才了解到 它们本是同源,js也是c系语言风格。 

           执行完后,通过当前绑定的线程池继续获取任务,获取到了任务则继续执行任务。没获取到任务,则修改相应状态和属性,并挂起当前抽象线程的线程句柄。  同时开始下一轮路径循环。

  从线程的角度来理解,本来一个线程要执行,它的代码应该是固定的,要么异常退出,要么代码执行完毕而结束。  而这里通过对线程的抽象,达到了动态执行分散代码块的效果。   这种类似的动态增删代码块的功能,我们也不是没有接触过,如 观察者模式,责任链模式,java中的动态代理也即所谓的 AOP,都可以完成类似的功能。 只不过,从抽象程度看,后者是在代码内部的抽象,而线程池是对线程的抽象。  从目的来看,后者是为了扩展伸缩,而线程池 是为了复用。 

  看完了这个工作线程,我们再来看看调度线程,因为从程序使用者角度,对 线程与 任务 二者的分别,是无感知的,例如本例的测试的代码: 

int main(){
    ThreadPool threadPool(2,6);
    for(size_t i=0;i<10;i++){
        threadPool.QueueTaskItem(Task::task1,NULL,TaskCallBack::taskCallback1);
    }
    threadPool.QueueTaskItem(Task::task1,NULL,TaskCallBack::taskCallback1,TRUE);

    //system("pause");
    return 0;
}

      在线程池开始调度程序的时候,与工作线程有区别,那就是它是新开了线程,便立即参与调度执行,它的工作函数如下: 

//从任务队列取任务的线程函数
 unsigned ThreadPool::GetTaskThreadProc(PVOID pM){
    ThreadPool* threadPool = (ThreadPool*)pM;
    BOOL bRet = FALSE;
    DWORD dwBytes = 0;
    WAIT_OPERATION_TYPE  opType;
    OVERLAPPED* ol;
    while(WAIT_OBJECT_0!=WaitForSingleObject(threadPool->stopEvent,0)){
        BOOL  bRet = GetQueuedCompletionStatus(threadPool->completionPort,&dwBytes,(PULONG_PTR)&opType,&ol,INFINITE);
        if(EXIT ==opType){
            break;
        } else if(GET_TASK ==opType){
            threadPool->getTaskExecute();
        }
    }
    return 0;
}

   注意到,该线程以接收到 stopEvent信号为结束,在信号量没有被激活的情况下,它可以类似的比作无限循环。 同时,在这个循环结构中,存在一个阻塞语句,该语句通过 完成端口 接收相应的信息(这个完成端口在windows用处挺大,后文也将进一步研究其与socket编程的结合使用。)。 当然了,接收到了信息,自然就是根据信息类别去做出相应的处理。 

   最后,再看一下 getTastExecute如何与 工作线程相互联系起来的。

void ThreadPool::getTaskExecute() {
    Thread *thread = NULL;
    WaitTask *waitTask = NULL;
    waitTask = getTask();
    if(waitTask ==NULL){
        return;
    }
    //如果是时常任务
    if(waitTask->bLong){
        if(idleThreadList.size()>minNumOfThread){
            thread = getIdleThread();
        }else{
            thread = new Thread(this);
            InterlockedIncrement(&numOfLongFun);
            InterlockedIncrement(&maxNumOfThread);
        }
    } else{//若不是时长任务
        thread = getIdleThread();
    }
    if(thread!=NULL){
        thread->executeTask(waitTask->task,waitTask->param,waitTask->taskCb);
        delete waitTask;
        moveThreadToBusyList(thread);
    } else{
        waitTaskLock.Lock();
        waitTaskList.push_front(waitTask);
        waitTaskLock.UnLock();
    }
}

     从代码中可以看出,它主要作用是 从空闲线程队列中,取出一个线程,取空闲线程的动作是线程安全的,如果空闲线程队列为空,且当前线程池中的线程不超过最大线程数的情况下,会新开一个线程。 然后调用该线程的执行任务的方法。 注意这一过程应该是异步的,并不是调度线程去执行,他只是设置好相应的参数,并触发线程。其代码如下:

void ThreadPool::Thread::executeTask(TaskFun task, PVOID param, TaskCallbackFun taskCallback) {
    busy = TRUE;
    this->task = task;
    this->param = param;
    this->taskCb = taskCallback;
    ResumeThread(thread);
}

   嗯,整体上就是这样的一个情况了。  呃,对了,任务没讲呢,很简单,其代码如下:

class Task {
public:
    static int task1(PVOID param);
};

class TaskCallBack{
public:
    static void taskCallback1(int result);
};

  没啥特殊要求,只要满足函数指针的 类型限定即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章