Linux多线程实践(9) --简单线程池的设计与实现
線程池的技術背景
? ?在面向對象編程中,創建和銷毀對象是很費時間的,因為創建一個對象要獲取內存資源或者其它更多資源。在Java中更是如此,虛擬機將試圖跟蹤每一個對象,以便能夠在對象銷毀后進行垃圾回收。所以提高服務程序效率的一個手段就是盡可能減少創建和銷毀對象的次數,特別是一些很耗資源的對象創建和銷毀。如何利用已有對象來服務(不止一個不同的任務)就是一個需要解決的關鍵問題,其實這就是一些"池化資源"技術產生的原因。比如大家所熟悉的數據庫連接池正是遵循這一思想而產生的,本文將介紹的線程池技術同樣符合這一思想。
? ?目前,一些著名的大公司都特別看好這項技術,并早已經在他們的產品中應用該技術。比如IBM的WebSphere,IONA的Orbix?2000在SUN的?Jini中,Microsoft的MTS(Microsoft?Transaction?Server?2.0),COM+等。
現在您是否也想在服務器程序應用該項技術?
?
線程池技術如何提高服務器程序的性能
? ?我所提到服務器程序是指能夠接受客戶請求并能處理請求的程序,而不只是指那些接受網絡客戶請求的網絡服務器程序。
? ?多線程技術主要解決處理器單元內多個線程執行的問題,它可以顯著減少處理器單元的閑置時間,增加處理器單元的吞吐能力。但如果對多線程應用不當,會增加對單個任務的處理時間。可以舉一個簡單的例子:
?
假設在一臺服務器完成一項任務的時間為T
T1?創建線程的時間?????????????????????????????????
T2?在線程中執行任務的時間,包括線程間同步所需時間?
T3?線程銷毀的時間 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??
顯然T?=?T1+T2+T3。注意這是一個極度簡化的假設。
? ?可以看出T1,T3是多線程本身的帶來的開銷,我們渴望減少T1,T3所用的時間,從而減少T的時間。但一些線程的使用者并沒有注意到這一點,所以在程序中頻繁的創建或銷毀線程,這導致T1和T3在T中占有相當比例(在傳統的多線程服務器模型中是這樣實現的:一旦有個請求到達,就創建一個新的線程,由該線程執行任務,任務執行完畢之后,線程就退出。這就是"即時創建,即時銷毀"的策略。盡管與創建進程相比,創建線程的時間已經大大的縮短,但是如果提交給線程的任務是執行時間較短,而且執行次數非常頻繁,那么服務器就將處于一個不停的創建線程和銷毀線程的狀態。這筆開銷是不可忽略的,尤其是線程執行的時間非常非常短的情況。)。顯然這是突出了線程的弱點(T1,T3),而不是優點(并發性)。
? ?線程池技術正是關注如何縮短或調整T1,T3時間的技術,從而提高服務器程序性能的。它把T1,T3分別安排在服務器程序的啟動和結束的時間段或者一些空閑的時間段(在應用程序啟動之后,就馬上創建一定數量的線程,放入空閑的隊列中。這些線程都是處于阻塞狀態,這些線程只占一點內存,不占用CPU。當任務到來后,線程池將選擇一個空閑的線程,將任務傳入此線程中運行。當所有的線程都處在處理任務的時候,線程池將自動創建一定的數量的新線程,用于處理更多的任務。執行任務完成之后線程并不退出,而是繼續在線程池中等待下一次任務。當大部分線程處于阻塞狀態時,線程池將自動銷毀一部分的線程,回收系統資源),這樣在服務器程序處理客戶請求時,不會有T1,T3的開銷了。
? ?線程池不僅調整T1,T3產生的時間段,而且它還顯著減少了創建線程的數目。再看一個例子:
? ?假設一個服務器一天要處理50000個請求,并且每個請求需要一個單獨的線程完成。我們比較利用線程池技術和不利于線程池技術的服務器處理這些請求時所產生的線程總數。在線程池中,線程數一般是固定的,所以產生線程總數不會超過線程池中線程的數目或者上限(以下簡稱線程池尺寸),而如果服務器不利用線程池來處理這些請求則線程總數為50000。一般線程池尺寸是遠小于50000。所以利用線程池的服務器程序不會為了創建50000而在處理請求時浪費時間,從而提高效率。
簡單線程池的實現
下面是一個簡單線程池的實現,?它所使用的方案如下:
? ?1.程序啟動之前,初始化線程池,此時線程池中沒有任何線程,?需要調用addTask方法向線程池中添加任務;
? ?2.如果此時線程池有空閑(處于等待)的線程,?就不會創建新的線程,?這樣就省去了T1,?T3的時間;
? ?3.如果此時線程池中沒有處于等待的線程(由于此時線程剛剛初始化,?此時線程池中肯定是沒有處于等待狀態的線程的)并且此時線程池中的線程數并沒有達到閾值,?才創建并啟動線程;?
? ?4.如果此時線程池中的線程數已經達到閾值,?那就只能等待現在還執行任務的線程,?等到其執行完其當前正在執行任務,?然后才從任務隊列中將新任務取出然后執行;
?
線程池主要由兩個文件組成,?一個threadpool.h頭文件和一個threadpool.cpp源文件組成。源碼中已有重要的注釋,就不加以分析了。
//ThreadPool設計 void *thread_routine(void *args); class ThreadPool {friend void *thread_routine(void *args); private://回調函數類型typedef void *(*callback_t)(void *);//任務結構體struct task_t{callback_t run; //任務回調函數void *args; //任務函數參數};public:ThreadPool(int _maxThreads = 36, unsigned int _waitSeconds = 2);~ThreadPool();//添加任務接口void addTask(callback_t run, void *args);private:void startTask();private:Condition ready; //任務準備就緒或線程池銷毀通知std::queue<task_t *> taskQueue; //任務隊列unsigned int maxThreads; //線程池最多允許的線程數unsigned int counter; //線程池當前線程數unsigned int idle; //線程池空閑線程數unsigned int waitSeconds; //線程可以等待的秒數bool quit; //線程池銷毀標志 }; //構造函數 ThreadPool::ThreadPool(int _maxThreads, unsigned int _waitSeconds): maxThreads(_maxThreads), counter(0), idle(0),waitSeconds(_waitSeconds), quit(false) {} // 線程入口函數 // 這其實就相當于一個消費者線程, 不斷的消費任務(執行任務) void *thread_routine(void *args) {//將子線程設置成為分離狀態, 這樣主線程就可以不用jionpthread_detach(pthread_self());printf("*thread 0x%lx is starting...\n", (unsigned long)pthread_self());ThreadPool *pool = (ThreadPool *)args;//等待任務的到來, 然后執行任務while (true){bool timeout = false;pool->ready.lock();//當處于等待的時候, 則說明空閑的線程多了一個++ pool->idle;//pool->ready中的條件變量有三個作用:// 1.等待任務隊列中有任務到來// 2.等待線程池銷毀通知// 3.確保當等待超時的時候, 能夠將線程銷毀(線程退出)while (pool->taskQueue.empty() && pool->quit == false){printf("thread 0x%lx is waiting...\n", (unsigned long)pthread_self());//等待waitSecondsif (0 != pool->ready.timedwait(pool->waitSeconds)){//如果等待超時printf("thread 0x%lx is wait timeout ...\n", (unsigned long)pthread_self());timeout = true;//break出循環, 繼續向下執行, 會執行到下面第1個if處break;}}//條件成熟(當等待結束), 線程開始執行任務或者是線程銷毀, 則說明空閑線程又少了一個-- pool->idle;// 狀態3.如果等待超時(一般此時任務隊列已經空了)if (timeout == true && pool->taskQueue.empty()){-- pool->counter;//解鎖然后跳出循環, 直接銷毀線程(退出線程)pool->ready.unlock();break;}// 狀態2.如果是等待到了線程的銷毀通知, 且任務都執行完畢了if (pool->quit == true && pool->taskQueue.empty()){-- pool->counter;//如果沒有線程了, 則給線程池發送通知//告訴線程池, 池中已經沒有線程了if (pool->counter == 0)pool->ready.signal();//解鎖然后跳出循環pool->ready.unlock();break;}// 狀態1.如果是有任務了, 則執行任務if (!(pool->taskQueue.empty())){//從隊頭取出任務進行處理ThreadPool::task_t *t = pool->taskQueue.front();pool->taskQueue.pop();//執行任務需要一定的時間//解鎖以便于其他的生產者可以繼續生產任務, 其他的消費者也可以消費任務pool->ready.unlock();//處理任務t->run(t->args);delete t;}}//跳出循環之后, 打印退出信息, 然后銷毀線程printf("thread 0x%lx is exiting...\n", (unsigned long)pthread_self());pthread_exit(NULL); } //addTask函數 //添加任務函數, 類似于一個生產者, 不斷的將任務生成, 掛接到任務隊列上, 等待消費者線程進行消費 void ThreadPool::addTask(callback_t run, void *args) {/** 1. 生成任務并將任務添加到"任務隊列"隊尾 **/task_t *newTask = new task_t {run, args};ready.lock(); //注意需要使用互斥量保護共享變量taskQueue.push(newTask);/** 2. 讓線程開始執行任務 **/startTask();ready.unlock();//解鎖以使任務開始執行 } //線程啟動函數 void ThreadPool::startTask() {// 如果有等待線程, 則喚醒其中一個, 讓它來執行任務if (idle > 0)ready.signal();// 沒有等待線程, 而且當前先線程總數尚未達到閾值, 我們就需要創建一個新的線程else if (counter < maxThreads){pthread_t tid;pthread_create(&tid, NULL, thread_routine, this);++ counter;} } //析構函數 ThreadPool::~ThreadPool() {//如果已經調用過了, 則直接返回if (quit == true)return;ready.lock();quit = true;if (counter > 0){//對于處于等待狀態, 則給他們發送通知,//這些處于等待狀態的線程, 則會接收到通知,//然后直接退出if (idle > 0)ready.broadcast();//對于正處于執行任務的線程, 他們接收不到這些通知,//則需要等待他們執行完任務while (counter > 0)ready.wait();}ready.unlock(); }完整源代碼:http://download.csdn.net/download/hanqing280441589/8449049
?
關于高級線程池的探討
? ?簡單線程池存在一些問題,比如如果有大量的客戶要求服務器為其服務,但由于線程池的工作線程是有限的,服務器只能為部分客戶服務,其它客戶提交的任務,只能在任務隊列中等待處理。一些系統設計人員可能會不滿這種狀況,因為他們對服務器程序的響應時間要求比較嚴格,所以在系統設計時可能會懷疑線程池技術的可行性,但是線程池有相應的解決方案。調整優化線程池尺寸是高級線程池要解決的一個問題。主要有下列解決方案:
?
方案一:動態增加工作線程
? ?在一些高級線程池中一般提供一個可以動態改變的工作線程數目的功能,以適應突發性的請求。一旦請求變少了將逐步減少線程池中工作線程的數目。當然線程增加可以采用一種超前方式,即批量增加一批工作線程,而不是來一個請求才建立創建一個線程。批量創建是更加有效的方式。該方案還有應該限制線程池中工作線程數目的上限和下限。否則這種靈活的方式也就變成一種錯誤的方式或者災難,因為頻繁的創建線程或者短時間內產生大量的線程將會背離使用線程池原始初衷--減少創建線程的次數。
? ?舉例:Jini中的TaskManager,就是一個精巧線程池管理器,它是動態增加工作線程的。SQL?Server采用單進程(Single?Process)多線程(Multi-Thread)的系統結構,1024個數量的線程池,動態線程分配,理論上限32767。
方案二:優化工作線程數目
? ?如果不想在線程池應用復雜的策略來保證工作線程數滿足應用的要求,你就要根據統計學的原理來統計客戶的請求數目,比如高峰時段平均一秒鐘內有多少任務要求處理,并根據系統的承受能力及客戶的忍受能力來平衡估計一個合理的線程池尺寸。線程池的尺寸確實很難確定,所以有時干脆用經驗值。
? ?舉例:在MTS中線程池的尺寸固定為100。?
方案三:一個服務器提供多個線程池
? ?在一些復雜的系統結構會采用這個方案。這樣可以根據不同任務或者任務優先級來采用不同線程池處理。
? ?舉例:COM+用到了多個線程池。
這三種方案各有優缺點。在不同應用中可能采用不同的方案或者干脆組合這三種方案來解決實際問題。
?
線程池技術適用范圍及應注意的問題
下面是我總結的一些線程池應用范圍,可能是不全面的。
線程池的應用范圍:
? ?(1)需要大量的線程來完成任務,且完成任務的時間比較短。?WEB服務器完成網頁請求這樣的任務,使用線程池技術是非常合適的。因為單個任務小,而任務數量巨大,你可以想象一個熱門網站的點擊次數。?但對于長時間的任務,比如一個Telnet連接請求,線程池的優點就不明顯了。因為Telnet會話時間比線程的創建時間大多了。
? ?(2)對性能要求苛刻的應用,比如要求服務器迅速相應客戶請求。
? ?(3)接受突發性的大量請求,但不至于使服務器因此產生大量線程的應用。突發性大量客戶請求,在沒有線程池情況下,將產生大量線程,雖然理論上大部分操作系統線程數目最大值不是問題,短時間內產生大量線程可能使內存到達極限,并出現"OutOfMemory"的錯誤。
?
結束語
? ?本文只是簡單介紹線程池技術。可以看出線程池技術對于服務器程序的性能改善是顯著的。線程池技術在服務器領域有著廣泛的應用前景。希望這項技術能夠應用到您的多線程服務程序中。
? ?注:這是網上一篇博客的改造:?將Java版本的線程池改造成了基于Linux?的C++版本,?原文鏈接為:http://www.ibm.com/developerworks/cn/java/l-threadPool/,?如果讀者的興趣所在為Java,?請移步于此,?向您鄭重推薦,?這是一篇非常好的文章,?謝謝!
總結
以上是生活随笔為你收集整理的Linux多线程实践(9) --简单线程池的设计与实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 免费下载精美网站模板的25个网站推荐
- 下一篇: Linux IPC实践(12) --Sy