程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> C++語言設計可擴展線程池

C++語言設計可擴展線程池

編輯:關於C++

在各種業務解決方案的設計過程中,服務器處理任務的效率往往決定了方案的成敗。多線程處理任務是提高服務器效率的主要手段,它提高了對服務器資源的利用,使得任務可以並發處理。但如果服務器處理的任務的特點是輕量級、頻率高,那麼線程的創建與銷毀會非常頻繁,而系統用於處理線程的創建與銷毀的開銷會占相當大的比重,反而降低了系統的效率。通過線程池技術,可以減少頻繁的線程的創建與銷毀對系統性能的影響。

線程池是預先創建線程的一種技術。線程池在還沒有任務到來之前,創建一定數量(N1)的線程,放入空閒隊列中。這些線程都是處於阻塞(Suspended)狀態,不消耗CPU,但占用較小的內存空間。當任務到來後,緩沖池選擇一個空閒線程,把任務傳入此線程中運行。當N1個線程都在處理任務後,緩沖池自動創建一定數量的新線程,用於處理更多的任務。當系統比較空閒時,大部分線程都一直處於暫停狀態,線程池自動銷毀一部分線程,回收系統資源。

通用線程緩沖池的設計,不僅要實現上述功能,還要考慮此設計的可移植性,減少重復開發。設計中需要考慮的重點是:

任務對象的通用性;

線程創建和銷毀策略;

任務的分配策略。

分析與設計

1、任務對象的通用性

不同的業務解決方案有各自獨特的任務處理方法,任務的劃分上也就千差萬別。為了使得在處理任務對象的時候達到一定程度的通用性,任務對象的設計上必須與實際任務的處理邏輯完全無關。從任務執行的角度看,任務不過是處理流程的一次或者多次執行的過程,可以這樣來定義任務接口:

class Task
{
public:
Task();
virtual ~Task();
virtual bool run() = 0;
};

Task類是所有任務類的基類,其中的純虛函數run()是任務流程的入口,工作線程在處理任務的時候就從此處開始執行任務的處理流程。設計一個新的任務時,只需要繼承Task接口,新的任務就可以放入線程池中執行。

任務的創建、執行和銷毀這樣來設計:

(1)任務在其需要的時候才創建。任務的創建通過new操作,動態創建具體的任務對象,然後傳入線程池,由線程池自動分配線程來執行此任務。

(2)任務是否執行完畢由其自身來決定。一個未知任務什麼時候執行完畢是不可能預測的,必須任務本身來決定。這個策略通過,Task::run()的返回值來實現。當工作線程執行一次任務時,如果返回值為true,表示任務執行完畢,就用delete操作銷毀此任務;如果返回值為false,表示任務需要執行的工作並未完成,繼續執行此任務。

這樣的策略,使得在設計新的任務處理流程的時候,不需要過多的關心任務的接口規范,只需要在新任務類的構造函數中初始化各種資源,在新任務類的析構函數中回收資源,在run()方法中實現主要的處理邏輯,那麼新的任務類即可在線程池中執行。

2、線程的創建與銷毀

線程緩沖池中的維持的線程數量應該按照任務處理的需求來定。

在緩沖池剛剛建立時,線程池中有一定數量(N1)的已創建好的線程,這樣可以使得新任務可以及時的得到執行。比如,某客戶端在向服務器發送登陸請求的時候,這樣一個請求使得服務器通常需要創建好幾個相互有關聯的任務。也就是說,客戶端與服務器端的一次交互,通常會產生一定數量的任務。根據一個服務器所處理的業務,估計出平均情況下,一次業務產生的任務數量N2。那麼N1應該是N2的整數倍,N1=N2×n1,減少由於線程不夠而再創建線程的概率,才能使得服務器在業務處理初期最為高效。

在線程緩沖池中的所有線程都處於繁忙狀態的時候,線程池就會創建新的線程,設創建N3個。由以上分析,為了減少由於線程不夠而再創建線程的概率,N3也應該是N2的整數倍,N3=N2×n2。

當服務器業務減少,出現大量線程閒置的情況,就應該銷毀一部分線程。很顯然,這裡應該使用超時策略,當某些線程在超過時間T仍然處於閒置狀態,就銷毀一部分空閒線程。設銷毀N4個空閒線程,為了減少由於線程不夠而再創建線程的概率,N4也應該是N2的整數倍,N4=N2×n3。當然,為了使得新任務及時得到處理,即使服務器一直處於空閒,也應該保留N1個線程。

3、任務分配策略

在業務處理中,會有各種各樣的任務對象,這些業務對象對系統資源的使用也不同。這些任務,無論其空間復雜度如何,從線程執行任務這一角度來看,應該關心的主要是時間復雜度。

線程緩沖池在接收到新任務的時候,首先要尋找空閒線程,傳入新任務,然後執行任務,最後還要刪除任務,置空閒線程的標志。尋找空閒線程、傳入任務、最後的清理工作,這些都是為了執行任務而產生的額外開銷,如果所執行的任務大多數都是輕量級任務,那麼額外開銷帶來的資源浪費就顯得很突出了。為了解決這個問題,可以給一個線程傳入N5個輕量級任務,這一個線程依次執行N5個輕量級任務,由於都是在很短時間內完成,並不影響任務響應的及時性。顯然,N5≥1。

實現

由於源代碼的篇幅關系,並不能把所有代碼一一列舉,這裡以偽代碼的形式給出線程緩沖池在線程的創建、銷毀、任務分配以及任務執行方面的流程。

(1) 線程池任務分配主循環(也是一個線程)

這裡除了任務分配算法外也包括了部分線程的創建與銷毀的算法。

for(;;) {
pThread = GetIdleThread();// 檢查空閒線程隊列
if( pThread != NULL ) {
if( CheckNewTask() ) {// 有新任務
TaskList tl;
GetTask( tl ); // 取得一定數量的任務
AddTaskToThread( pTask, tl );// 把任務傳入線程
continue; // 繼續循環
}
}
if( pThread == NULL && nThread < THREAD_MAX )// 沒有空閒線程了
CreateNewThread();// 創建新線程
continue;// 繼續循環
}
// 沒有要處理的任務或者已經到達線程數的上限,進入超時等待
if( WaitForTaskOrThreadTimeout() ) {
if( IncrIdleTime() > IDLE_MAX ) { // 系統空閒,計時
// 系統長時間處於空閒,銷毀一定數量的空閒線程
DecrIdleThread();
}
}
else
return 0;// 線程終止
}

(2) 工作線程的任務執行流程

for(;;) {
// 檢查任務隊列是否有任務要運行
if( !CheckTaskQueue() ) { // 隊列中沒有任務
pPool->OnTaskIdle( this ); // 通知線程池,此線程已經空閒
if( WaitForTask() )
continue;// 繼續循環
else
return 0;// 終止線程
} else { // 有任務需要運行
pTask = GetTask(); // 取得新任務
try {
while( !pTask->Run() ) {
// 此處循環體為空,不斷運行直到任務執行完畢
}
}
catch( … ) {
WriteLog( … ); // 執行任務時產生異常,記錄入日志
}
delete pTask; // 任務執行完畢,刪除此任務
}
}

在任務執行的核心部分,使用了try-catch控制塊進行異常捕獲。雖然異常會對程序速度有很略微的影響,但是因為要執行的任務是未知的,不能保證任務可以正常執行。因為一個任務的異常而導致服務器的服務程序崩潰,這是絕對不允許的。使用異常捕獲不僅可以保證服務器流程的順利執行,而且把異常信息存入日志文件,還可以跟蹤錯誤。

性能測試

為了檢驗此線程池的性能是否和預期相同,並且分析出線程池的不同參數配置對系統性能的影響,特編寫了測試程序對三組參數進行了測試,測試結果如圖1所示:

橫坐標是任務數量;縱坐標是消耗時間,以秒(s)為單位。

參數1:N2 = 1, N5 = 1; 參數2:N2 = 5, N5 = 1; 參數3:N2 = 5, N5 = 5

測試中,系統的總的線程數限制為500,任務都是5ms。這裡只針對N2和N5進行測試,N2是平均情況下系統每次向線程池中增加的任務數量,N5是每個線程一次執行任務數量。

在任務量比較小的情況下,三者的對系統性能的占用基本上相等。但是當任務量很巨大的時候,參數1比參數2效率要稍微高出一些,而參數3的執行效率幾乎是前兩者的一倍。

因為都是輕量級任務,所以N2的變化對系統效率的影響並不大,而N5的影響就很顯著。

結束語

通過測試可以看出,在服務器中使用線程池後,並不意味著系統性能就一定可以提升。不同系統的任務有著各自不同的特點,這就需要根據服務器任務的特點進一步調整緩沖池的一些關鍵參數,才能最大程度的提高系統效率。這些參數就是上面分析過程中的N1、N2、N3、N4、N5、n1、n2、n3。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved