執行緒池
參考:
1、遊雙Linux高效能伺服器程式設計
注:雖然是"從0開始",但最好對(多)執行緒、執行緒同步等知識點有所瞭解再看,不然可能有些地方會理解不到位(但也有可能是我沒說明到位,水平有限,見諒)
Web伺服器與執行緒池的關係
Web伺服器需要同時處理多個客戶端請求,並且每個請求可能需要花費很長時間來處理,如資料庫查詢、檔案讀寫等操作。
因此Web伺服器不太可能是單執行緒的,要實現併發操作就必須引入多執行緒技術
使用執行緒池的伺服器屬於多執行緒伺服器。執行緒池本質上是一種多執行緒技術,透過在程式啟動時建立一定數量的工作執行緒,並將所有請求任務加入到任務佇列中,以便執行緒池中的多個工作執行緒可以同時處理請求任務。
在Web伺服器中,執行緒池的作用是有效地處理併發請求,提高伺服器的併發能力和效能。
具體地,Web伺服器透過執行緒池實現以下幾個方面的功能:
- 提高併發效能:執行緒池可以在同一時刻處理多個請求,從而提高伺服器的併發處理能力,減少請求響應時間。
- 控制執行緒數量:執行緒池可以控制同時執行的執行緒數量,從而避免執行緒數目過多導致系統資源耗盡。
- 管理執行緒狀態:執行緒池可以對工作執行緒進行管理,並監測工作執行緒的狀態,如是否空閒、是否存活等。
- 避免執行緒建立銷燬開銷:使用執行緒池可以避免頻繁地建立和銷燬執行緒,從而減少系統開銷,提高伺服器的效能。
實現一個執行緒池
前置知識:互斥鎖、訊號量、C++模板程式設計
(注:程式碼中使用的)
執行緒池在程式碼實現上是一個模板類,使用模板的原因是方便進行復用
執行緒池模板類,主要用於管理多個執行緒並處理任務。其中包含以下主要成員變數和函式:
- 成員變數:執行緒數量(m_thread_number)、最大請求數(m_max_requests)、執行緒池陣列(m_threads)、請求佇列(m_workqueue)、互斥鎖(m_queuelocker)、訊號量(m_queuestat)和是否停止標誌(m_stop)。
- 成員函式:建構函式(threadpool())、解構函式(~threadpool())、新增任務(append())、子執行緒中要執行的工作函式(worker())和啟動執行緒池中的一個或多個執行緒進行任務處理的函式(run())。
下面是該程式碼的工作流程和原理:
1、建立執行緒池
建立時,會先判斷執行緒數和最大請求數是否小於等於0。之後,會建立一個大小為 m_thread_number 的執行緒池陣列,並將其設定為執行緒脫離狀態,即在建立完執行緒之後就可以將其與當前的程式分離,避免執行緒阻塞主執行緒和其他相關執行緒的執行。如果建立時失敗,則丟擲異常。
2、新增任務
往任務佇列中新增請求時,會先加鎖(使用互斥鎖),以確保多個程式不會爭搶。之後,會判斷任務佇列中的請求數是否超過 m_max_requests,如果佇列大小大於最大請求數,則解鎖並返回 false。否則,將請求新增到佇列中,解鎖並增加訊號量,通知執行緒池中的執行緒,有新任務需要處理。最終返回 true。
3、執行緒工作函式
當收到訊號量後,執行緒會先上鎖(使用互斥鎖),以確保多個執行緒不會同時訪問佇列。之後,會判斷任務佇列是否為空。如果佇列為空,則解鎖並繼續等待下一次訊號量的到來。否則,取出佇列頂部的請求,並將其彈出佇列。最後,解鎖,並呼叫任務函式(request->process())。
4、解構函式
執行解構函式時,將 m_stop 設定為 true,供執行緒判斷是否要停止。
定義執行緒池類
先來定義一下執行緒池類
template<typename T> threadpool {
private:
int m_thread_number;//執行緒數
int m_max_requests;//最大請求數
bool m_stop;//停止符
pthread_t* m_thread;//執行緒池陣列
std::list<T*> m_workqueue;//任務佇列
locker queuelocker;//互斥鎖,locker是對系統提供的mutex方法的封裝,位於locker.h中
sem m_queuestat;//訊號量
private:
static void* worker(void* arg);//工作函式
void run(); //執行緒池的主函式,用於檢查任務佇列中的請求
public:
threadpool(int m_thread_number = 8, int m_max_requests = 1000);//構造
~threadpool();//析構
bool append();//將請求加入m_workqueue中
}
其實也沒有很複雜,但是有以下幾個點需要注意:
1、執行緒池陣列m_thread和任務佇列m_workqueue是沒有直接聯絡的
線上程池類初始化時,執行緒池陣列就會根據指定的 執行緒數m_thread_number 來建立對應數量的執行緒
這些執行緒會被阻塞(一直呼叫 run函式 檢查 m_workqueue 中有無請求),直到 **外部呼叫執行緒池物件的程式碼 **透過呼叫threadpool<T>
類的物件的append()
方法 向任務佇列中新增新的任務
這時,append()
才會定義模板類中宣告的 任務佇列m_workqueue(也就是往佇列中push請求任務)
此時,之前被建立的某個執行緒檢查到佇列中有任務,於是其獲取到了鎖
將佇列中的請求彈出,呼叫對應的任務函式進行處理
綜上,雖然執行緒池陣列和任務佇列沒有直接聯絡,但是他們具有協作關係,實現伺服器對於請求的監聽和處理操作
2、工作函式為什麼要定義為靜態的,並且其返回值為什麼是void*
?
在C語言中,函式的返回值只能是一個型別。但是有些時候,我們需要從一個函式中返回多個值或者不同型別的值。
在這種情況下,我們可以使用指標或者void指標來達到這個目的。
在這裡,worker被定義為static void*
,其中void*
表示一個指向記憶體地址的指標,但是它沒有指定具體的資料型別。
這意味著worker函式可以返回任何型別的指標,這使得worker函式具有更大的靈活性,並且可以處理各種不同型別的資料。
同時,透過將worker函式宣告為static,它只能在當前檔案中使用,可以避免與其他檔案中的函式名稱重複的問題。
好了,執行緒池模板類定義完了,現在需要在類外分別實現各個成員函式
按順序來:建構函式->解構函式->...
實現執行緒池建構函式
建構函式負責根據給定的thread_number來建立執行緒
首先,我們需要判斷所給的引數範圍是否合法
然後建立執行緒池陣列,大小為thread_number,最後呼叫pthread_create函式建立執行緒,使用pthread_detach函式將執行緒設定為脫離狀態
template<typename T> //透過引數列表進行初始化
threadpool<T>::threadpool(int thread_number, int max_requests):m_thread_number(thread_number),m_max_requests(max_requests),
m_stop(false), m_threads(NULL){
//異常判斷,執行緒數和最大請求數小於0,報錯
if((thread_number <= 0) || (max_requests <= 0)){
throw std:: exception();
}
m_threads = new pthread_t[m_thread_number];//建立執行緒池陣列
if(!m_threads){
throw std:: exception();
}
for(int i = 0; i < thread_number; ++i){
printf("建立第 %d 個執行緒\n", i);
if(pthread_create(m_threads + i, NULL, worker, this) != 0){
delete[] m_threads;
throw std::exception();//建立失敗
}
if(pthread_detach(m_threads[i])){//在呼叫pthread_detach()函式之後,執行緒將進入“分離”狀態,這意味著它不能再被其他執行緒或主執行緒等待和加入。
}
}
}
從建構函式可知,執行緒是線上程池建立時就被建立的,並且數量是固定的
有以下注意點:
1、pthread_create函式的傳入引數
pthread_create()函式需要四個輸入引數,分別是:
1.執行緒識別符號指標(pthread_t *),用於儲存新建立執行緒的識別符號;
2.執行緒屬性指標(const pthread_attr_t *),用於設定新執行緒的屬性。如果不需要設定,則可以將該引數設定為NULL;
3.指向函式的指標(void (start_routine) (void *)),用於作為新執行緒的入口點。新執行緒開始執行時會從該函式開始執行;
4.傳遞給新執行緒入口點函式的引數指標(void *),該引數可以是任意型別的指標,它會被傳遞給新執行緒入口點函式。
在本程式碼中,pthread_create()函式的第一個引數是一個pthread_t型別的指標,該指標用於儲存新建立執行緒的識別符號。
第二個引數設定為NULL,因為我們不需要設定新執行緒的屬性。
第三個引數是一個指向worker函式的指標,作為新執行緒的入口點。
最後一個引數是一個指向當前threadpool物件的指標,它被傳遞給worker函式作為引數,讓worker函式能夠訪問到threadpool物件的所有
成員。
m_threads + i 表示將 m_threads指標 向後偏移 i 個 pthread_t型別的長度,即指向執行緒池中第i個工作執行緒的識別符號。
m_threads 是一個指向pthread_t型別的陣列,當使用 m_threads[i] 時,實際上是對m_threads陣列中第i個元素進行訪問。
因此,m_threads + i 表示對 m_threads陣列 進行偏移,使其指向第i個元素的地址。
在pthread_create()
函式中,需要傳遞一個指向執行緒識別符號的指標作為引數,來儲存新建執行緒的識別符號。
因此,可以使用 m_threads + i 作為該引數,表示將指向第i個工作執行緒的識別符號的地址傳遞給pthread_create()
函式。
worker是一個靜態成員函式,它作為執行緒執行的入口點,用於處理任務佇列中的請求。
this指標是一個指向當前threadpool物件的指標,它被傳遞給worker函式作為引數。
由於worker函式是靜態的,因此無法訪問threadpool物件的非靜態成員。
(為了防止重名所以設為靜態)
因此,需要將threadpool物件的指標作為引數傳遞給worker函式,以便讓worker函式能夠訪問threadpool物件的所有成員。
在pthread_create()函式中,需要將worker函式的指標作為引數傳遞,而this指標則用於向worker函式傳遞threadpool物件的指標。
透過這種方式,就可以讓worker函式訪問到threadpool物件的所有成員變數和成員函式。
解構函式
主要作用就是停止執行緒池。刪除執行緒池陣列,並回收資源
//實現解構函式
template<typename T>
threadpool<T>::~threadpool(){
delete[] m_threads;//用完之後就把執行緒池陣列刪除
m_stop = true;//執行解構函式時將其置為true,供執行緒判斷是否要停止
}
實現工作函式worker
從上面的分析可知,worker函式 會線上程池初始化時被建構函式呼叫
template<typename T>
void* threadpool<T>::worker(void* arg){
threadpool* pool = (threadpool* )arg;
pool->run();
return pool;
}
該函式主要做的事情就是就是接收一個執行緒池物件
具體來說, worker函式 從 pthread_create()函式 中得到了一個void型別的指標作為輸入引數。
這個指標可以透過 pthread_create() 的最後一個引數(也就是 this )進行傳遞,即執行緒建立時呼叫的arg引數。
什麼意思呢?
我們要呼叫pthread_create()函式建立一個執行緒,此時我們需要提供執行緒的儲存位置、執行緒屬性引數、新執行緒的入口函式以及該入口函式所需的引數的指標
在建立執行緒時,需要傳入一個入口函式的指標,用於告訴作業系統新執行緒應該從哪裡開始執行程式程式碼。在這裡,我們將worker函式作為入口函式,以便啟動一個新執行緒,並且讓該執行緒執行執行緒池的工作函式run()。
也就是說,我們在例項化一個執行緒池類的時候(假設為A),同時也把一個指向A的指標作為引數傳給了worker函式(透過pthread_create()函式),worker函式再呼叫A中的成員函式run()用於處理任務
由於run()函式被定義為非靜態成員函式,無法直接作為入口函式使用,因此我們選擇了worker函式作為入口函式,並在其中呼叫run()函式。在C++中,可以將任何函式作為新執行緒的入口函式,只要它符合執行緒函式的格式要求(即返回值為void*
型別,引數為void*
型別)。在實際程式設計中,通常會選擇一個適當的函式作為入口函式,以實現所需的功能。
實現任務處理函式run
新執行緒以worker函式為入口進行執行後,會透過指標去呼叫run()函式,透過迴圈不斷檢查任務佇列來獲取任務(如果有的話)
取到任務之後會呼叫相應的處理函式進行處理(這裡是process(),還沒定義)
template<typename T>
void threadpool<T>::run(){
while(!m_stop){
//阻塞等待捕獲sem訊號量
m_queuestat.wait();
//拿到訊號量之後上鎖
m_queuelocker.lock();
if(m_workqueue.empty()){
m_queuelocker.unlock();//若佇列為空就解鎖
continue;
}
//取出佇列頭部的請求
T* request = m_workqueue.front();
m_workqueue.pop_front();
m_queuelocker.unlock();//解鎖
if(!request) continue;//沒有東西就繼續迴圈
request->process();//有就呼叫對應的處理函式
}
}
這裡用到了互斥鎖與訊號量
上鎖是為了保證在多執行緒的情況下,不會出現資源爭奪的情況,保證執行緒安全
訊號量的使用則涉及到了伺服器整體的設計結構,後面再說(面試重點)
實現新增任務函式append
到目前為止,一個所謂的"執行緒池"已經基本完工
這個"池"可以建立一個陣列存放建立好的執行緒物件,並維護一個任務佇列,從佇列中不斷檢查是否有新任務(外界對伺服器的請求)到來
從程式碼來看,run函式只有在收到訊號量時才會去檢查佇列
那麼是誰負責傳送(改變)訊號量呢?那肯定是負責將任務加入佇列的那個部分,也就是append函式了
template<typename T>
void threadpool::append(T* request){
//觸發append就意味著有新請求來了,此時需要有執行緒來處理,所以為了安全要上鎖
m_queuelocker.lock();
//判斷當前佇列中的任務是否已經達到最大請求上限
if(m_workqeue.size() > m_max_requests){
m_queuelocker.unlock();//是就解鎖
return false;//新增失敗
}
m_workqueue.push_back(request);//往任務佇列新增一個請求
m_queuelocker.unlock();
m_queuestat.post();//修改訊號量
return true;
}
在向佇列新增任務後,append修改了訊號量m_queuestat,使得阻塞在wait()處的run函式開始檢查佇列,獲取剛被加入到佇列中的任務
至此,執行緒池的全部功能實現完畢
完整程式碼
threadpool.h
#ifndef THREADPOOL_H
#define THREADPOOL_H
#include <pthread.h>
#include <list>
#include <stdio.h>
#include "locker.h"
//執行緒池類,將其定義為模板類是為了程式碼的複用
//模板引數T就是任務類
template<typename T>
class threadpool {
private:
//執行緒數量
int m_thread_number;
//執行緒池陣列,大小為m_thread_number
pthread_t * m_threads;//使用pthread_t一是為了效能,二是為了執行緒安全(相對於vector來說)
//請求佇列中最多允許的待處理請求數
int m_max_requests;
//請求佇列
std::list<T*> m_workqueue;
//互斥鎖
locker m_queuelocker;
//訊號量,用於判斷是否有任務需要處理
sem m_queuestat;
//是否結束執行緒
bool m_stop;
private:
//子執行緒中要執行的程式碼
static void* worker(void* arg);
void run();
public:
threadpool(int thread_number = 8, int max_request = 10000);
~threadpool();
bool append(T* request);
};
//模板外實現執行緒池建構函式
template<typename T>//引數列表初始化
threadpool<T>::threadpool(int thread_number, int max_requests):
m_thread_number(thread_number),m_max_requests(max_requests),
m_stop(false), m_threads(NULL){
//異常判斷,執行緒數和最大請求數小於0,報錯
if((thread_number <= 0) || (max_requests <= 0)){
throw std:: exception();
}
m_threads = new pthread_t[m_thread_number];//建立執行緒池陣列
if(!m_threads){
throw std:: exception();
}
//建立thread_number個執行緒,並將它們設定為執行緒脫離
//執行緒脫離指的是在一個多執行緒程式中,某個執行緒完成了它原本需要執行的任務之後,
//並不立即結束自己的執行,而是繼續保持執行狀態,直到其他執行緒也完成了它們的任務之後才退出。
//這種情況下,該執行緒被稱為“脫離執行緒”(detached thread)
/*執行緒脫離通常用於需要長時間執行的後臺任務,透過將這些任務單獨分配給脫離執行緒來處理,可以避免阻塞主執行緒和其他相關執行緒的執行。*/
for(int i = 0; i < thread_number; ++i){
printf("建立第 %d 個執行緒\n", i);
//C++裡面的woker是靜態的,所以要傳入this來訪問類裡變數
/*在C++中,對指標進行加減操作會根據指標型別的大小進行調整。
因此,m_threads + i表示將m_threads指標向後偏移i個pthread_t型別的長度,即指向執行緒池中第i個工作執行緒的識別符號。
m_threads是一個指向pthread_t型別的陣列,當使用m_threads[i]時,實際上是對m_threads陣列中第i個元素進行訪問。
m_threads + i表示對m_threads陣列進行偏移,使其指向第i個元素的地址。
在pthread_create()函式中,需要傳遞一個指向執行緒識別符號的指標作為引數,來儲存新建執行緒的識別符號。
因此,可以使用m_threads + i作為該引數,表示將指向第i個工作執行緒的識別符號的地址傳遞給pthread_create()函式。*/
if(pthread_create(m_threads + i, NULL, worker, this) != 0){//為了讓worker訪問非靜態成員,傳入this
delete[] m_threads;
throw std::exception();//建立失敗
}
if(pthread_detach(m_threads[i])){//在呼叫pthread_detach()函式之後,執行緒將進入“分離”狀態,這意味著它不能再被其他執行緒或主執行緒等待和加入。
}
}
}
//實現解構函式
template<typename T>
threadpool<T>::~threadpool(){
delete[] m_threads;//用完之後就把執行緒池陣列刪除
m_stop = true;//執行解構函式時將其置為true,供執行緒判斷是否要停止
}
//實現append
template<typename T>
bool threadpool<T>::append(T* request){//往佇列中新增任務,要保證執行緒同步
m_queuelocker.lock();//新增互斥鎖
if(m_workqueue.size() > m_max_requests){//任務佇列大小大於最大請求數
m_queuelocker.unlock();//解鎖並報錯,此時的任務數已經超出上限
return false;
}
m_workqueue.push_back(request);//往佇列中增加一個請求
m_queuelocker.unlock();//解鎖
//將請求加入工作佇列的操作是需要保證其原子性的,因此需要互斥鎖保證多個程式不會爭搶
m_queuestat.post();//增加訊號量,通知執行緒池中的執行緒,有新任務需要處理
return true;
/*當一個新的任務被新增到佇列中時,會呼叫 m_queuestat.post() 增加訊號量。
線上程池初始化時,每個工作執行緒都被建立並阻塞在 m_queuestat.wait() 上等待訊號量的觸發。
一旦 m_queuestat 的值大於 0,其中的一個執行緒就會從阻塞狀態喚醒並開始處理佇列中的請求。*/
}
template<typename T>//執行緒池的工作函式,其中模板引數T未被使用。該函式是作為新執行緒啟動時呼叫的入口函式
void* threadpool<T>::worker(void* arg){
// 傳入void 型別指標 arg
/*arg 是在啟動執行緒時傳遞給該執行緒函式的引數。
以下程式碼中,它被轉換為 threadpool* 型別,因為它實際上是一個指向 threadpool 結構體的指標。
然後,將這個指標賦值給名為 pool 的變數,以便在該函式中訪問和操作 threadpool 結構體的成員。*/
threadpool* pool = (threadpool* ) arg;//在pthread_create中傳入worker
pool->run();//啟動執行緒池中的一個或多個執行緒,並將待處理任務提交給執行緒池進行處理
return pool;
}
template<typename T>
void threadpool<T>::run(){
while(!m_stop){
m_queuestat.wait();//等待append函式傳過來的訊號量,收到表示需要執行執行緒池,使用其中的執行緒處理來處理任務
//可能有資料到了,上鎖
/*關於為什麼這裡要上鎖:
收到訊號量時,任務佇列 m_workqueue 可能為空,也可能不為空,這取決於在等待訊號量之前是否有新任務被新增到了佇列中。
如果沒有新任務被新增,那麼 m_workqueue 仍然為空。如果有新任務被新增,那麼 m_workqueue 將不為空。
需要注意的是,在多執行緒程式設計中,一個執行緒在等待訊號量時,另一個執行緒可能會往任務佇列中新增新任務,因此需要透過加鎖(比如互斥鎖)來保證對任務佇列的訪問是執行緒安全的。
這樣可以避免出現競態條件(race condition,也就是執行緒不同步),從而確保程式的正確性。
*/
m_queuelocker.lock();
/*在等待訊號量的執行緒執行之前,如果沒有任何其他執行緒向任務佇列中新增新的任務,那麼收到訊號量時 m_workqueue 可能為空。這種情況可以出現在以下幾種情況下:
在初始化程式時,建立了一個空的任務佇列並等待訊號量,此時 m_workqueue 為空。
所有的任務都已經被處理完畢,並且等待訊號量的執行緒尚未收到新的任務新增進來。
等待訊號量的執行緒剛剛完成了處理該任務佇列中的所有任務,然後又立即等待訊號量,此時 m_workqueue 為空。
*/
if(m_workqueue.empty()){
m_queuelocker.unlock();//解鎖
continue;//繼續迴圈,檢視佇列中是否有資料
}
//取出佇列頂部的請求,並將其彈出佇列
T* request = m_workqueue.front();
m_workqueue.pop_front();
//取完請求後,解鎖
m_queuelocker.unlock();
if(!request){
continue;//沒獲取到就繼續迴圈
}
//呼叫任務函式
request->process();
}
}
#endif
locker.h
#ifndef LOCKER_H //沒定義就定義一個LOCKER_H
#define LOCKER_H
#include <pthread.h>//互斥鎖相關
#include <exception>
#include <semaphore.h>//訊號量相關
//執行緒頭部機制的封裝類
//互斥鎖類
class locker{
private:
pthread_mutex_t m_mutex;//建立一個互斥鎖
public:
locker(){//建構函式
if(pthread_mutex_init(&m_mutex, NULL) != 0){//初始化一個互斥鎖,預設屬性
throw std::exception();//丟擲異常
}
}
~locker(){//解構函式,銷燬
pthread_mutex_destroy(&m_mutex);
}
bool lock(){//上鎖
return pthread_mutex_lock(&m_mutex) == 0;//判斷當前執行緒是否成功獲取到了互斥鎖 m_mutex。如果返回值為0,則表示當前執行緒已經成功獲取到了該互斥鎖;如果返回值不為0,則表示當前執行緒未能獲取到該互斥鎖。
}
bool unlock(){
return pthread_mutex_unlock(&m_mutex) == 0;
}
pthread_mutex_t * get(){//獲取互斥量
return &m_mutex;
}
};
//條件變數類
//判斷佇列中有無資料,沒有就讓執行緒停著,有就喚醒執行緒
class cond {
private:
pthread_cond_t m_cond;//建立一個條件變數
public:
cond(){//建構函式
if (pthread_cond_init(&m_cond, NULL) != 0) {
throw std::exception();
}
}
~cond() {//解構函式
pthread_cond_destroy(&m_cond);
}
bool wait(pthread_mutex_t *m_mutex) {
int ret = 0;
ret = pthread_cond_wait(&m_cond, m_mutex);
return ret == 0;
}
bool timewait(pthread_mutex_t *m_mutex, struct timespec t) {//超時
int ret = 0;
ret = pthread_cond_timedwait(&m_cond, m_mutex, &t);
return ret == 0;
}
bool signal() {//喚醒一個或多個執行緒
return pthread_cond_signal(&m_cond) == 0;
}
bool broadcast() {//喚醒所有執行緒
return pthread_cond_broadcast(&m_cond) == 0;
}
};
//訊號量類
class sem{
private:
sem_t m_sem;
public:
sem(){
if(sem_init(&m_sem, 0, 0) != 0){
throw std:: exception();
}
}
~sem(){
sem_destroy(&m_sem);
}
//等待訊號量
bool wait(){
return sem_wait(&m_sem) == 0;
}
//增加訊號量
bool post(){
return sem_post(&m_sem) == 0;
}
};
#endif