服務器程序利用線程技術響應客戶請求已經司空見慣,可能您認為這樣做效率已經很高,但您有沒有想過優化一下使用線程的方法。該文章將向您介紹服務器程序如何利用線程池來優化性能并提供一個簡單的線程池實現。
線程池的技術背景
在面向對象編程中,創建和銷毀對象是很費時間的,因為創建一個對象要獲取內存資源或者其它更多資源。在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分別安排在服務器程序的啟動和結束的時間段或者一些空閑的時間段,這樣在服務器程序處理客戶請求時,不會有T1,T3的開銷了。
線程池不僅調整T1,T3產生的時間段,而且它還顯著減少了創建線程的數目。在看一個例子:
假設一個服務器一天要處理50000個請求,并且每個請求需要一個單獨的線程完成。我們比較利用線程池技術和不利于線程池技術的服務器處理這些請求時所產生的線程總數。在線程池中,線程數一般是固定的,所以產生線程總數不會超過線程池中線程的數目或者上限(以下簡稱線程池尺寸),而如果服務器不利用線程池來處理這些請求則線程總數為50000。一般線程池尺寸是遠小于50000。所以利用線程池的服務器程序不會為了創建50000而在處理請求時浪費時間,從而提高效率。
這些都是假設,不能充分說明問題,下面我將討論線程池的簡單實現并對該程序進行對比測試,以說明線程技術優點及應用領域。
線程池的簡單實現及對比測試
一般一個簡單線程池至少包含下列組成部分。
- 線程池管理器(ThreadPoolManager):用于創建并管理線程池
- 工作線程(WorkThread): 線程池中線程
- 任務接口(Task):每個任務必須實現的接口,以供工作線程調度任務的執行。
- 任務隊列:用于存放沒有處理的任務。提供一種緩沖機制。
線程池管理器至少有下列功能:創建線程池,銷毀線程池,添加新任務創建線程池的部分代碼如下:
…
//create threads
synchronized(workThreadVector)
{
for(int j = 0; j < i; j++)
{
threadNum++;
WorkThread workThread = new WorkThread(taskVector, threadNum);
workThreadVector.addElement(workThread);
}
}
…
|
注意同步workThreadVector并沒有降低效率,相反提高了效率,請參考Brian Goetz的文章。 銷毀線程池的部分代碼如下:
…
while(!workThreadVector.isEmpty())
{
if(debugLevel > 2)
System.out.println("stop:"+(i));
i++;
try
{
WorkThread workThread = (WorkThread)workThreadVector.remove(0);
workThread.closeThread();
continue;
}
catch(Exception exception)
{
if(debugLevel > 2)
exception.printStackTrace();
}
break;
}
…
|
添加新任務的部分代碼如下:
…
synchronized(taskVector)
{
taskVector.addElement(taskObj);
taskVector.notifyAll();
}
…
|
工作線程是一個可以循環執行任務的線程,在沒有任務時將等待。由于代碼比較多在此不羅列.
任務接口是為所有任務提供統一的接口,以便工作線程處理。任務接口主要規定了任務的入口,任務執行完后的收尾工作,任務的執行狀態等。在文章結尾有相關代碼的下載。
以上所描述的線程池結構很簡單,一些復雜的線程池結構將不再此討論。
在下載代碼中有測試驅動程序(TestThreadPool),我利用這個測試程序的輸出數據統計出下列測試結果。測試有兩個參數要設置:
- 線程池中線程數,即線程池尺寸。
- 要完成的任務數。
分別將一個參數固定,另一個參數變動以考察兩個參數所產生的不同結果。所用測試機器分別為普通PC機(Win2000 JDK1.3.1)和SUN服務器(Solaris Unix JDK1.3.1),機器配置在此不便指明。
表1:測試數據及對應結果
線程池尺寸 |
任務數 |
沒有應用線程池所用的時間(單位:毫秒,OS:win) |
應用線程池所用的時間(單位:毫秒,OS:win) |
沒有應用線程池所用的時間(單位:毫秒,OS:Solaris) |
應用線程池所用的時間(單位:毫秒,OS:Solaris) |
1 |
5000 |
3896 |
130 |
6513 |
327 |
2 |
5000 |
3455 |
151 |
6221 |
659 |
4 |
5000 |
3425 |
120 |
5448 |
433 |
8 |
5000 |
3475 |
160 |
5769 |
1478 |
16 |
5000 |
3505 |
211 |
5785 |
1970 |
32 |
5000 |
3455 |
251 |
6403 |
875 |
64 |
5000 |
3595 |
501 |
5182 |
1103 |
128 |
5000 |
3515 |
881 |
5154 |
405 |
256 |
5000 |
3495 |
3104 |
5502 |
1589 |
512 |
5000 |
3425 |
5488 |
5667 |
1262 |
16 |
1 |
20 |
0 |
22 |
3 |
16 |
2 |
20 |
20 |
21 |
13 |
16 |
4 |
20 |
10 |
27 |
10 |
16 |
8 |
20 |
20 |
22 |
24 |
16 |
16 |
30 |
20 |
29 |
48 |
16 |
32 |
40 |
20 |
46 |
108 |
16 |
64 |
60 |
20 |
72 |
199 |
16 |
128 |
110 |
20 |
148 |
335 |
16 |
256 |
201 |
20 |
252 |
132 |
16 |
512 |
411 |
40 |
522 |
382 |
16 |
1024 |
811 |
71 |
1233 |
610 |
16 |
2048 |
1552 |
80 |
2045 |
135 |
16 |
4096 |
2874 |
250 |
4828 |
787 |
圖1.線程池的尺寸的對服務器程序的性能影響
根據以上統計數據可得出下圖:
圖2.任務數對服務器程序的沖擊
數據分析如下:
圖1是改變線程池尺寸對服務器性能的影響,在該測試過程中,服務器的要完成的任務數固定為為5000。從圖1中可以看出合理配置線程池尺寸對于大量任務處理的效率有非常明顯的提高,但是一旦尺寸選擇不合理(過大或過小)就會嚴重降低影響服務器性能。理論上"過小"將出現任務不能及時處理的情況,但在圖表中顯示出某些小尺寸的線程池表現很好,這是因為測試驅動中有很多線程同步開銷,且這個開銷相對于完成單個任務的時間是不能忽略的。"過大"則會出現線程間同步開銷太大的問題,而且在線程間切換很耗CPU時間,在圖表顯示的很清楚。可見任何一個好技術,如果濫用都會造成災難性后果。
圖2是用不同數量的任務來沖擊服務器程序,在該測試過程中,服務器線程池尺寸固定為16。可以看出線程池在處理少量任務時的優勢不明顯。所以線程池技術有一定的適應范圍,關于適用范圍將在后面討論。但對于大量的任務的處理,線程池的優勢表現非常卓越,服務器程序處理請求的時間雖然有波動,但是其平均值相對小多了。
值得注意的是測試方案中,統計任務的完成時間沒有包含了創建線程池的時間。在實際線程池工作時,即利用線程池處理任務時,創建線程池的時間是不必計算在內的。
由于測試驅動程序有很多同步代碼,特別是等待線程執行完畢的同步(代碼中為sleepToWait(long l)方法的調用),這些代碼降低了代碼執行效率,這是測試驅動一個缺點,但這個測試驅動可以說明線程池相對于簡單使用線程的優勢。
關于高級線程池的探討
簡單線程池存在一些問題,比如如果有大量的客戶要求服務器為其服務,但由于線程池的工作線程是有限的,服務器只能為部分客戶服務,其它客戶提交的任務,只能在任務隊列中等待處理。一些系統設計人員可能會不滿這種狀況,因為他們對服務器程序的響應時間要求比較嚴格,所以在系統設計時可能會懷疑線程池技術的可行性,但是線程池有相應的解決方案。調整優化線程池尺寸是高級線程池要解決的一個問題。主要有下列解決方案:
方案一:動態增加工作線程
在一些高級線程池中一般提供一個可以動態改變的工作線程數目的功能,以適應突發性的請求。一旦請求變少了將逐步減少線程池中工作線程的數目。當然線程增加可以采用一種超前方式,即批量增加一批工作線程,而不是來一個請求才建立創建一個線程。批量創建是更加有效的方式。該方案還有應該限制線程池中工作線程數目的上限和下限。否則這種靈活的方式也就變成一種錯誤的方式或者災難,因為頻繁的創建線程或者短時間內產生大量的線程將會背離使用線程池原始初衷--減少創建線程的次數。
舉例:Jini中的TaskManager,就是一個精巧線程池管理器,它是動態增加工作線程的。SQL Server采用單進程(Single Process)多線程(Multi-Thread)的系統結構,1024個數量的線程池,動態線程分配,理論上限32767。
方案二:優化工作線程數目
如果不想在線程池應用復雜的策略來保證工作線程數滿足應用的要求,你就要根據統計學的原理來統計客戶的請求數目,比如高峰時段平均一秒鐘內有多少任務要求處理,并根據系統的承受能力及客戶的忍受能力來平衡估計一個合理的線程池尺寸。線程池的尺寸確實很難確定,所以有時干脆用經驗值。
舉例:在MTS中線程池的尺寸固定為100。
方案三:一個服務器提供多個線程池
在一些復雜的系統結構會采用這個方案。這樣可以根據不同任務或者任務優先級來采用不同線程池處理。
舉例:COM+用到了多個線程池。
這三種方案各有優缺點。在不同應用中可能采用不同的方案或者干脆組合這三種方案來解決實際問題。
線程池技術適用范圍及應注意的問題
下面是我總結的一些線程池應用范圍,可能是不全面的。
線程池的應用范圍:
- 需要大量的線程來完成任務,且完成任務的時間比較短。 WEB服務器完成網頁請求這樣的任務,使用線程池技術是非常合適的。因為單個任務小,而任務數量巨大,你可以想象一個熱門網站的點擊次數。 但對于長時間的任務,比如一個Telnet連接請求,線程池的優點就不明顯了。因為Telnet會話時間比線程的創建時間大多了。
- 對性能要求苛刻的應用,比如要求服務器迅速相應客戶請求。
- 接受突發性的大量請求,但不至于使服務器因此產生大量線程的應用。突發性大量客戶請求,在沒有線程池情況下,將產生大量線程,雖然理論上大部分操作系統線程數目最大值不是問題,短時間內產生大量線程可能使內存到達極限,并出現"OutOfMemory"的錯誤。
本文只是簡單介紹線程池技術。可以看出線程池技術對于服務器程序的性能改善是顯著的。線程池技術在服務器領域有著廣泛的應用前景。希望這項技術能夠應用到您的多線程服務程序中。
|