掰開揉碎了教你設(shè)計(jì)線程池!還不來學(xué)?
大家好,我是作者小杰,我在學(xué)習(xí)線程池的時(shí)候也曾查閱過各種資料,但是感覺大佬寫的很好但是寫的不夠詳細(xì),寫的詳細(xì)的設(shè)計(jì)思路又很簡單,所以我的出發(fā)點(diǎn)就是讓讀者可以清晰明確的看懂整個設(shè)計(jì)思想和設(shè)計(jì)過程,可以舉一反三,在今后內(nèi)存池等方面也可以游刃有余的設(shè)計(jì)出來!好了,正文開始~
線程池設(shè)計(jì)思路
線程池是什么
我們先來打個比方,線程池就好像一個工具箱,我們每次需要擰螺絲的時(shí)候都要從工具箱里面取出一個螺絲刀來,有時(shí)候需要取出一個來擰,有時(shí)候螺絲多的時(shí)候需要多個人取出多個來擰,擰完自己的螺絲那么就會把螺絲刀再放回去,然后別人下次用的時(shí)候再取出來用。也許我的例子不是太完美,但是我想我已經(jīng)基本闡述清楚了線程池。說白了線程池就是相當(dāng)于提前申請了一些資源也就是線程,需要的時(shí)候就從線程池中取出線程來處理一些事情,處理完畢之后再把線程放回去。
線程池介紹
為什么需要線程池
我們來思考一個問題,為什么需要線程池呢?假如沒有線程池的話我們每次調(diào)用線程是什么樣子的?顯然首先是先創(chuàng)建一個線程,然后再把任務(wù)交給這個線程,最后再把這個線程銷毀掉。那么如果我們改用線程池的話,我們在程序運(yùn)行的時(shí)候就會首先創(chuàng)建一批線程,然后交給線程池來管理。有需要的時(shí)候我們把線程拿出去處理任務(wù),不需要的時(shí)候我們再回收到線程池中,這樣是不是就避免了每次都需要創(chuàng)建和銷毀線程這種消耗時(shí)間的操作。有人會說你使用線程池一開始就消耗了一些內(nèi)存,之后一直不釋放這些內(nèi)存,這樣豈不是有點(diǎn)浪費(fèi)。其實(shí)這是類似于空間換時(shí)間的概念,我們確實(shí)多占用了一點(diǎn)內(nèi)存但是這些內(nèi)存和我們珍惜出來的這些時(shí)間相比,是非常劃算的。
池的概念是一種非常常見的空間換時(shí)間的概念,除了有線程池之外還有進(jìn)程池、內(nèi)存池等等。其實(shí)他們的思想都是一樣的就是我先申請一批資源出來,然后就隨用隨拿,不用再放回來。聽到這兒是不是有種云計(jì)算的思想了,他們道理都是一樣的。
如何設(shè)計(jì)線程池
現(xiàn)在硬核的知識要開始了,請坐穩(wěn)扶好、抓緊扶手~
二話不說,先上圖看看,我們要設(shè)計(jì)的線程池長什么樣子!
線程池的設(shè)計(jì)
設(shè)計(jì)思路
我們需要一個線程池類,那么線程池類中都需要哪些東西呢?我們庖丁解牛來看一看
- 我們需要存放我們創(chuàng)建好的線程,因此我們需要一個容器專門放線程
- 需要一個容器來存放我們的任務(wù),每次把任務(wù)放到這個容器里面
- 由于是多線程的讀取任務(wù),所以必不可少的我們需要鎖,每次讀取任務(wù)需要加鎖和解鎖
- 我們需要判斷什么時(shí)候終止,因此還需要一個判斷終止的變量
為了避免輪詢的判斷任務(wù)集裝箱里面是不是空的,這樣效率太低了,因此我們這里采用條件變量
這里來說明一下什么是條件變量。條件變量是并發(fā)編程中的一種同步機(jī)制,條件變量使得線程能夠阻塞到等待某個條件發(fā)生后,再繼續(xù)執(zhí)行,期間還會把之前拿到的鎖先釋放掉,不影響其它人拿這把鎖。因此條件變量十分強(qiáng)大而高效。(條件變量和鎖將會在我多線程文章中詳細(xì)講解,這里不是重點(diǎn),所以不再展開細(xì)講)
接下來我們來研究一下線程池中需要有哪些操作呢?
- 將任務(wù)添加到線程池中的操作,并且這時(shí)應(yīng)該通知線程可以來取任務(wù)來執(zhí)行了
- 一個循環(huán)操作,不斷地等待任務(wù)集裝箱里面有數(shù)據(jù)來執(zhí)行,也就是初始化完畢后需要做的事情
- 通過改變終止變量來讓上面循環(huán)停止的操作
好了,到此已經(jīng)詳細(xì)的把設(shè)計(jì)思路寫清楚了,接下來該看具體的實(shí)現(xiàn)了
線程池的實(shí)現(xiàn)
接下來先來看一看線程池類是怎么實(shí)現(xiàn)的,注釋已經(jīng)很詳細(xì)了,就不多說了直接上代碼。
- class CThreadMangerPool
- {
- public:
- CThreadMangerPool(void):is_runing(false){};
- bool init(int threadnum);//初始化函數(shù)
- ~CThreadMangerPool(void);
- void Run(void); //執(zhí)行函數(shù)
- void stop(void); //用來終止循環(huán)的函數(shù)
- void addTask(ThreadTask* task);//向任務(wù)集裝箱中添加任務(wù)的函數(shù)
- private:
- bool CreateThreads(int threadnum = 5);
- std::vector<std::shared_ptr<std::thread>> threadsPool; //線程集裝箱,用來存放線程
- std::list<std::shared_ptr<ThreadTask>> threadTaskList; //任務(wù)集裝箱,用來存放線程執(zhí)行的任務(wù)
- std::condition_variable threadPool_cv; //條件變量
- std::mutex threadMutex; //互斥鎖
- //std::vector<std::shared_ptr<CTcpClient>> tcpClients;
- bool is_runing; //終止變量
- };
我們來幾個重點(diǎn)的函數(shù)實(shí)現(xiàn)~
在Run函數(shù)中,我們設(shè)計(jì)了一個循環(huán),不斷地執(zhí)行等待并取出任務(wù)執(zhí)行,如果沒有的任務(wù)可以執(zhí)行的話就睡眠等待(用之前提到的條件變量來實(shí)現(xiàn))
注意這里使用了一個手法,我們用while來判斷任務(wù)集裝箱中的數(shù)據(jù)是不是空的,是因?yàn)轭愃朴谶M(jìn)程的驚群現(xiàn)象,這里出現(xiàn)條件變量的虛假喚醒。(在這里并不是重點(diǎn)就不展開講了,會在我文章的多線程處詳細(xì)講解)
- void CThreadMangerPool::Run(){
- std::shared_ptr<ThreadTask> task;
- while(true){ //處在循環(huán)中
- std::unique_lock<std::mutex> guard(threadMutex);//利用RALL來管理鎖,不用手動釋放
- while(threadTaskList.empty()){ // 這里防止條件變量的虛假喚醒,所以不用if判斷
- if (!is_runing)
- break;
- threadPool_cv.wait(guard); //條件變量的使用
- }
- if (!is_runing) //同上 都是判斷如果未啟動或者調(diào)用了stop函數(shù)都會退出循環(huán)
- break;
- task = threadTaskList.front(); //取出任務(wù)
- threadTaskList.pop_front(); //把任務(wù)從容器中拿走
- if (task == NULL)
- continue;
- task->DoIt(); //執(zhí)行任務(wù)處理函數(shù)
- task.reset(); //重置指針
- }
- }
接下來看看增加任務(wù)的函數(shù)是怎么實(shí)現(xiàn)的
- void CThreadMangerPool::addTask(ThreadTask* task){
- std::shared_ptr<ThreadTask> ptr; //創(chuàng)建一個指向任務(wù)的智能指針
- ptr.reset(task);
- {
- std::lock_guard<std::mutex> guard(threadMutex); //同樣是用RALL來管理鎖,免去手動釋放
- threadTaskList.push_back(ptr); //往任務(wù)集裝箱中添加任務(wù)
- }
- threadPool_cv.notify_all(); //通知線程可以執(zhí)行了,就是喚醒剛才在條件變量處睡眠的條件
- }
好了,重點(diǎn)函數(shù)已經(jīng)看完了,其他的輕松就可以實(shí)現(xiàn)包括初始化函數(shù),終止函數(shù)等等
完結(jié)撒花~