一個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);
};

  沒啥特殊要求,只要滿足函數指針的 類型限定即可。

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