目錄
Linux線程池
線程池的概念
線程池的優點
線程池的應用場景
線程池的實現
Linux線程池
線程池的概念
線程池是一種線程的使用模式。
其存在的主要原因就為:線程過多會帶來調度開銷,進而影響緩存局部性和整體性能。而線程池維護著多個線程,等待著監督管理者分配可并發執行的任務。
線程池的優點
- 線程池避免了再處理短時間任務時創建于銷毀線程的代價。
- 線程池不僅能夠保證內核的充分利用,還能防止過分調度。?
注意:線程池可用線程數量應該取決于可用的并發處理器、處理器內核、內存、網絡sockets等的數量。
線程池的應用場景
線程池常見的應用場景如下:
- 需要大量的線程來完成任務,且完成任務的時間比較短。
- 對性能要求苛刻的應用,比如要求服務器迅速響應客戶請求。
- 接受突發性的大量請求,但不至于使服務器因此產生大量線程的應用。
相關現實場景應用:
- WEB服務器完成網頁請求這樣的任務,使用線程池技術是非常合適的。因為單個任務小,而任務數量巨大,你可以想象一個熱門網站的點擊次數。
- 但對于長時間的任務,比如一個Telnet連接請求,線程池的優點就不明顯了。因為Telnet會話時間比線程的創建時間大多了。
- 突發性大量客戶請求,在沒有線程池情況下,將產生大量線程,雖然理論上大部分操作系統線程數目最大值不是問題,短時間內產生大量線程可能使內存到達極限,出現錯誤.
線程池的實現
下面我們用圖示,表達一個簡單的線程池,線程池中提供了一個任務隊列,以及若干個線程(多線程)。
- 線程池中的多個線程負責從任務隊列當中拿任務,并將拿到的任務進行處理。
- 線程池對外提供一個Push接口,用于讓外部線程能夠將任務Push到任務隊列當中。
線程池的代碼如下:
.hpp? (ThreadPool.hpp)
#include <iostream>
#include <unistd.h>
#include <queue>
#include <stdlib.h>
#include <pthread.h>class ThreadInfo
{
public:pthread_t tid_;std::string name_;
};static const int defalutnum = 5;template <class T>
class ThreadPool
{
public:void Lock(){pthread_mutex_lock(&mutex_);}void Unlock(){pthread_mutex_unlock(&mutex_);}void Wakeup(){pthread_cond_signal(&cond_);}void ThreadSleep(){pthread_cond_wait(&cond_, &mutex_);}bool IsQueueEmpty(){return tasks_.empty();}std::string GetThreadName(pthread_t tid){for(const auto& ti:threads_){if(ti.tid_ == tid){return ti.name_;}}return "None";}
public:static void *HandlerTask(void *args) // 帶 static 是因為類內的函數第一個參數是默認為 this 指針{ThreadPool<T> *tp = static_cast<ThreadPool*>(args);std::string name = tp->GetThreadName(pthread_self());while(true){tp->Lock();while(tp->IsQueueEmpty()){tp->ThreadSleep();}// 分配到任務T t = tp->Pop();std::cout<< 分配到了一個任務 : " << t << "," << name << "run" << std::endl;tp->Unlock();sleep(1);// 后續的任務處理部分// run();}}void Start(){int num = defalutnum;for(int i=0; i < num; i++){threads_[i].name_ = "thread-" + std::to_string(i + 1);pthread_create(&(threads_[i].tid_), nullptr, HandlerTask, this);}}T Pop(){T t = tasks_.front();tasks_.pop();return t;}void Push(const T &t){Lock();tasks_.push(t);Wakeup();Unlock();}
public:ThreadPool(int num = defalutnum) :threads_(num){pthread_mutex_init(&mutex_, nullptr);pthread_cond_init(&cond_, nullptr);}~ThreadPool(){pthread_mutex_destroy(&mutex_);pthread_cond_destroy(&cond_);}
private:std::vector<ThreadInfo> threads_;std::queue<T> tasks_;pthread_mutex_t mutex_;pthread_cond_t cond_;
};
知識回顧:為什么線程池中需要有互斥鎖和條件變量?
因為線程池中的任務隊列是會被多個執行流同時訪問的臨界資源,因此為了避免數據二義性,就會創建互斥鎖與條件變量來進行保護。
線程池當中的線程要從任務隊列里拿任務,前提條件是任務隊列中必須要有任務,因此線程池當中的線程在拿任務之前,需要先判斷任務隊列當中是否有任務,若此時任務隊列為空,那么該線程應該進行等待,直到任務隊列中有任務時再將其喚醒,因此我們需要引入條件變量。
當外部線程向任務隊列中Push一個任務后,此時可能有線程正處于等待狀態,因此在新增任務后需要喚醒在條件變量下等待的線程。
注意:
- 當某線程被喚醒時,其可能是被異常或是偽喚醒,或者是一些廣播類的喚醒線程操作而導致所有線程被喚醒,使得在被喚醒的若干線程中,只有個別線程能拿到任務。此時應該讓被喚醒的線程再次判斷是否滿足被喚醒條件,所以在判斷任務隊列是否為空時,應該使用while進行判斷,而不是if。
- 這里的喚醒是用的pthread_cond_signal而不是pthread_cond_broadcast,那是因為,如果外部向隊列中push了一個任務,但我們卻使用pthread_cond_broadcast,將所有的等待線程全部喚醒,但實際情況卻僅需要一個線程,那這肯定會資源的浪費。一瞬間喚醒大量的線程可能會導致系統震蕩,這叫做驚群效應。所以在喚醒線程時最好還是使用pthread_cond_signal函數喚醒一個正在等待的線程即可。
- 當線程從任務隊列中拿到任務后,該任務就已經屬于當前線程了,與其他線程已經沒有關系了。因此應該在解鎖之后再進行處理任務,而不是在解鎖之前進行。因為處理任務的過程可能會耗費一定的時間,所以我們不要將其放到臨界區當中。
- 還有就是設計的效率問題,如果我們將處理任務的操作也放在臨界區來處理,那么當某一線程從任務隊列中拿到任務后,其他線程還需要等待該線程將任務處理完后,才有機會進入臨界區。這樣的設計雖然也叫做線程池,但顯然效率是遠沒有僅僅將拿取任務部分放在臨界區的效率高。
?為什么線程池中的線程執行例程需要設置為靜態方法?
使用pthread_create函數創建線程時,需要為創建的線程傳入一個HandlerTask(執行例程),該HandlerTask只有一個參數類型為void*的參數,以及返回類型為void*的返回值。
而此時HandlerTask作為類的成員函數,該函數的第一個參數是隱藏的this指針,因此這里的HandlerTask函數,雖然看起來只有一個參數,而實際上它有兩個參數,此時直接將該HandlerTask函數作為創建線程時的執行例程是不行的,無法通過編譯。
靜態成員函數屬于類,而不屬于某個對象,也就是說靜態成員函數是沒有隱藏的this指針的,因此我們需要將HandlerTask設置為靜態方法,此時HandlerTask函數才真正只有一個參數類型為void*的參數。
但是在靜態成員函數內部無法調用非靜態成員函數,而我們需要在HandlerTask函數當中調用該類的某些非靜態成員函數,比如Pop。因此我們需要在創建線程時,向Routine函數傳入的當前對象的this指針,此時我們就能夠通過該this指針在HandlerTask函數內部調用非靜態成員函數了。
?主函數:.cpp? (proc.cc)
#include <iostream>
#include <ctime>
#include "ThreadPool.hpp"int main()
{std::cout << "process running..." << std::endl;sleep(3);ThreadPool<int> *tp = new ThreadPool<int>();tp->Start();srand(time(nullptr) ^ getpid());while(true){//1. 構建任務int x = rand() % 10 + 1;tp->Push(x);//2. 交給線程池處理std::cout << "proc thread make task: " << std::endl;sleep(1);}
}
?makefile
Threadpool:proc.ccg++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm -f Threadpool
?運行效果如下: