多程序與多執行緒的選擇與區別
魚還是熊掌:淺談多程序多執行緒的選擇
關於多程序和多執行緒,教科書上最經典的一句話是“程序是資源分配的最小單位,執行緒是CPU排程的最小單位”,這句話應付考試基本上夠了,但如果在工作中遇到類似的選擇問題,那就沒有這麼簡單了,選的不好,會讓你深受其害。
經常在網路上看到有的XDJM問“多程序好還是多執行緒好?”、“Linux下用多程序還是多執行緒?”等等期望一勞永逸的問題,我只能說:沒有最好,只有更好。根據實際情況來判斷,哪個更加合適就是哪個好。
我們按照多個不同的維度,來看看多執行緒和多程序的對比(注:因為是感性的比較,因此都是相對的,不是說一個好得不得了,另外一個差的無法忍受)。
對比維度 |
多程序 |
多執行緒 |
總結 |
資料共享、同步 |
資料共享複雜,需要用IPC;資料是分開的,同步簡單 |
因為共享程序資料,資料共享簡單,但也是因為這個原因導致同步複雜 |
各有優勢 |
記憶體、CPU |
佔用記憶體多,切換複雜,CPU利用率低 |
佔用記憶體少,切換簡單,CPU利用率高 |
執行緒佔優 |
建立銷燬、切換 |
建立銷燬、切換複雜,速度慢 |
建立銷燬、切換簡單,速度很快 |
執行緒佔優 |
程式設計、除錯 |
程式設計簡單,除錯簡單 |
程式設計複雜,除錯複雜 |
程序佔優 |
可靠性 |
程序間不會互相影響 |
一個執行緒掛掉將導致整個程序掛掉 |
程序佔優 |
分散式 |
適應於多核、多機分散式;如果一臺機器不夠,擴充套件到多臺機器比較簡單 |
適應於多核分散式 |
程序佔優 |
看起來比較簡單,優勢對比上是“執行緒 3.5 v 2.5 程序”,我們只管選執行緒就是了?
呵呵,有這麼簡單我就不用在這裡浪費口舌了,還是那句話,沒有絕對的好與壞,只有哪個更加合適的問題。我們來看實際應用中究竟如何判斷更加合適。
1)需要頻繁建立銷燬的優先用執行緒
原因請看上面的對比。
這種原則最常見的應用就是Web伺服器了,來一個連線建立一個執行緒,斷了就銷燬執行緒,要是用程序,建立和銷燬的代價是很難承受的
2)需要進行大量計算的優先使用執行緒
所謂大量計算,當然就是要耗費很多CPU,切換頻繁了,這種情況下執行緒是最合適的。
這種原則最常見的是影象處理、演算法處理。
3)強相關的處理用執行緒,弱相關的處理用程序
什麼叫強相關、弱相關?理論上很難定義,給個簡單的例子就明白了。
一般的Server需要完成如下任務:訊息收發、訊息處理。“訊息收發”和“訊息處理”就是弱相關的任務,而“訊息處理”裡面可能又分為“訊息解碼”、“業務處理”,這兩個任務相對來說相關性就要強多了。因此“訊息收發”和“訊息處理”可以分程序設計,“訊息解碼”、“業務處理”可以分執行緒設計。
當然這種劃分方式不是一成不變的,也可以根據實際情況進行調整。
4)可能要擴充套件到多機分佈的用程序,多核分佈的用執行緒
原因請看上面對比。
5)都滿足需求的情況下,用你最熟悉、最拿手的方式
至於“資料共享、同步”、“程式設計、除錯”、“可靠性”這幾個維度的所謂的“複雜、簡單”應該怎麼取捨,我只能說:沒有明確的選擇方法。但我可以告訴你一個選擇原則:如果多程序和多執行緒都能夠滿足要求,那麼選擇你最熟悉、最拿手的那個。
需要提醒的是:雖然我給了這麼多的選擇原則,但實際應用中基本上都是“程序+執行緒”的結合方式,千萬不要真的陷入一種非此即彼的誤區。
1、程序與執行緒
程序是程式執行時的一個例項,即它是程式已經執行到課中程度的資料結構的彙集。從核心的觀點看,程序的目的就是擔當分配系統資源(CPU時間、記憶體等)的基本單位。
執行緒是程序的一個執行流,是CPU排程和分派的基本單位,它是比程序更小的能獨立執行的基本單位。一個程序由幾個執行緒組成(擁有很多相對獨立的執行流的使用者程式共享應用程式的大部分資料結構),執行緒與同屬一個程序的其他的執行緒共享程序所擁有的全部資源。
"程序——資源分配的最小單位,執行緒——程式執行的最小單位"
程序有獨立的地址空間,一個程序崩潰後,在保護模式下不會對其它程序產生影響,而執行緒只是一個程序中的不同執行路徑。執行緒有自己的堆疊和區域性變數,但執行緒沒有單獨的地址空間,一個執行緒死掉就等於整個程序死掉,所以多程序的程式要比多執行緒的程式健壯,但在程序切換時,耗費資源較大,效率要差一些。但對於一些要求同時進行並且又要共享某些變數的併發操作,只能用執行緒,不能用程序。
總的來說就是:程序有獨立的地址空間,執行緒沒有單獨的地址空間(同一程序內的執行緒共享程序的地址空間)。(下面的內容摘自Linux下的多執行緒程式設計)
使用多執行緒的理由之一是和程序相比,它是一種非常"節儉"的多工操作方式。我們知道,在Linux系統下,啟動一個新的程序必須分配給它獨立的地址空間,建立眾多的資料表來維護它的程式碼段、堆疊段和資料段,這是一種"昂貴"的多工工作方式。而運行於一個程序中的多個執行緒,它們彼此之間使用相同的地址空間,共享大部分資料,啟動一個執行緒所花費的空間遠遠小於啟動一個程序所花費的空間,而且,執行緒間彼此切換所需的時間也遠遠小於程序間切換所需要的時間。據統計,總的說來,一個程序的開銷大約是一個執行緒開銷的30倍左右,當然,在具體的系統上,這個資料可能會有較大的區別。
使用多執行緒的理由之二是執行緒間方便的通訊機制。對不同程序來說,它們具有獨立的資料空間,要進行資料的傳遞只能通過通訊的方式進行,這種方式不僅費時,而且很不方便。執行緒則不然,由於同一程序下的執行緒之間共享資料空間,所以一個執行緒的資料可以直接為其它執行緒所用,這不僅快捷,而且方便。當然,資料的共享也帶來其他一些問題,有的變數不能同時被兩個執行緒所修改,有的子程式中宣告為static的資料更有可能給多執行緒程式帶來災難性的打擊,這些正是編寫多執行緒程式時最需要注意的地方。
除了以上所說的優點外,不和程序比較,多執行緒程式作為一種多工、併發的工作方式,當然有以下的優點:
- 提高應用程式響應。這對圖形介面的程式尤其有意義,當一個操作耗時很長時,整個系統都會等待這個操作,此時程式不會響應鍵盤、滑鼠、選單的操作,而使用多執行緒技術,將耗時長的操作(time consuming)置於一個新的執行緒,可以避免這種尷尬的情況。
- 使多CPU系統更加有效。作業系統會保證當執行緒數不大於CPU數目時,不同的執行緒運行於不同的CPU上。
- 改善程式結構。一個既長又複雜的程序可以考慮分為多個執行緒,成為幾個獨立或半獨立的執行部分,這樣的程式會利於理解和修改。
在Unix上程式設計採用多執行緒還是多程序的爭執由來已久,這種爭執最常見到在B/S通訊中服務端併發技術 的選型上,比如WEB伺服器技術中,Apache是採用多程序的(perfork模式,每客戶連線對應一個程序,每程序中只存在唯一一個執行線 程),Java的Web容器Tomcat、Websphere等都是多執行緒的(每客戶連線對應一個執行緒,所有執行緒都在一個程序中)。
從Unix發展歷史看,伴隨著Unix的誕生多程序就出現了,而多執行緒很晚才被系統支援,例如Linux直到核心2.6,才支援符合Posix規範的NPTL執行緒庫。程序和執行緒的特點,也就是各自的優缺點如下:
程序優點:程式設計、除錯簡單,可靠性較高。
程序缺點:建立、銷燬、切換速度慢,記憶體、資源佔用大。
執行緒優點:建立、銷燬、切換速度快,記憶體、資源佔用小。
執行緒缺點:程式設計、除錯複雜,可靠性較差。
上面的對比可以歸結為一句話:“執行緒快而程序可靠性高”。執行緒有個別名叫“輕量級程序”,在有的書籍資料上介紹執行緒可以十倍、百倍的效率快於程序; 而程序之間不共享資料,沒有鎖問題,結構簡單,一個程序崩潰不像執行緒那樣影響全域性,因此比較可靠。我相信這個觀點可以被大部分人所接受,因為和我們所接受的知識概念是相符的。
在寫這篇文章前,我也屬於這“大部分人”,這兩年在用C語言編寫的幾個C/S通訊程式中,因時間緊總是採用多程序併發技術,而且是比較簡單的現場為 每客戶fork()一個程序,當時總是擔心併發量增大時負荷能否承受,盤算著等時間充裕了將它改為多執行緒形式,或者改為預先建立程序的形式,直到最近在網 上看到了一篇論文《Linux系統下多執行緒與多程序效能分析》作者“周麗 焦程波 蘭巨龍”,才認真思考這個問題,我自己也做了實驗,結論和論文作者的相似,但對大部分人可以說是顛覆性的。
下面是得出結論的實驗步驟和過程,結論究竟是怎樣的? 感興趣就一起看看吧。
實驗程式碼使用周麗論文中的程式碼樣例,我做了少量修改,值得注意的是這樣的區別:
論文實驗和我的實驗時間不同,論文所處的年代linux核心是2.4,我的實驗linux核心是2.6,2.6使用的執行緒庫是NPTL,2.4使用的是老的Linux執行緒庫(用程序模擬執行緒的那個LinuxThread)。
論文實驗和我用的機器不同,論文描述了使用的環境:單cpu 機器基本配置為:celeron 2.0 GZ, 256M, Linux 9.2,核心 2.4.8。我的環境是:雙核 Intel(R) Xeon(R) CPU 5130 @ 2.00GHz(做實驗時,禁掉了一核),512MG記憶體,Red Hat Enterprise Linux ES release 4 (Nahant Update 4),核心2.6.9-42。
程序實驗程式碼(fork.c):
- #include <stdlib.h>
- #include <stdio.h>
- #include <signal.h>
- #define P_NUMBER 255 //併發程序數量
- #define COUNT 5 //每次程序列印字串數
- #define TEST_LOGFILE "logFile.log"
- FILE *logFile=NULL;
- char *s="hello linux\0";
- int main()
- {
- int i=0,j=0;
- logFile=fopen(TEST_LOGFILE,"a+");//開啟日誌檔案
- for(i=0;i<P_NUMBER;i++)
- {
- if(fork()==0)//建立子程序,if(fork()==0){}這段程式碼是子程序執行區間
- {
- for(j=0;j<COUNT;j++)
- {
- printf("[%d]%s\n",j,s);//向控制檯輸出
- /*當你頻繁讀寫檔案的時候,Linux核心為了提高讀寫效能與速度,會將檔案在記憶體中進行快取,這部分記憶體就是Cache Memory(快取記憶體)。可能導致測試結果不準,所以在此註釋*/
- //fprintf(logFile,"[%d]%s\n",j,s);//向日志文件輸出,
- }
- exit(0);//子程序結束
- }
- }
- for(i=0;i<P_NUMBER;i++)//回收子程序
- {
- wait(0);
- }
- printf("Okay\n");
- return 0;
- }
程序實驗程式碼(thread.c):
- #include <pthread.h>
- #include <unistd.h>
- #include <stdlib.h>
- #include <stdio.h>
- #define P_NUMBER 255//併發執行緒數量
- #define COUNT 5 //每執行緒列印字串數
- #define TEST_LOG "logFile.log"
- FILE *logFile=NULL;
- char *s="hello linux\0";
- print_hello_linux()//執行緒執行的函式
- {
- int i=0;
- for(i=0;i<COUNT;i++)
- {
- printf("[%d]%s\n",i,s);//想控制檯輸出
- /*當你頻繁讀寫檔案的時候,Linux核心為了提高讀寫效能與速度,會將檔案在記憶體中進行快取,這部分記憶體就是Cache Memory(快取記憶體)。可能導致測試結果不準,所以在此註釋*/
- //fprintf(logFile,"[%d]%s\n",i,s);//向日志文件輸出
- }
- pthread_exit(0);//執行緒結束
- }
- int main()
- {
- int i=0;
- pthread_t pid[P_NUMBER];//執行緒陣列
- logFile=fopen(TEST_LOG,"a+");//開啟日誌檔案
- for(i=0;i<P_NUMBER;i++)
- pthread_create(&pid[i],NULL,(void *)print_hello_linux,NULL);//建立執行緒
- for(i=0;i<P_NUMBER;i++)
- pthread_join(pid[i],NULL);//回收執行緒
- printf("Okay\n");
- return 0;
- }
兩段程式做的事情是一樣的,都是建立“若干”個程序/執行緒,每個創建出的程序/執行緒列印“若干”條“hello linux”字串到控制檯和日誌檔案,兩個“若干”由兩個巨集 P_NUMBER和COUNT分別定義,程式編譯指令如下:
gcc -o fork fork.c
gcc -lpthread -o thread thread.c
實驗通過time指令執行兩個程式,抄錄time輸出的掛鐘時間(real時間):
time ./fork
time ./thread
每批次的實驗通過改動巨集 P_NUMBER和COUNT來調整程序/執行緒數量和列印次數,每批次測試五輪,得到的結果如下:
一、重複周麗論文實驗步驟
(注:本文平均值演算法採用的是去掉一個最大值去掉一個最小值,然後平均)
單核(雙核機器禁掉一核),程序/執行緒數:255,列印次數5 |
||||||
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均 |
|
多程序 |
0m0.070s |
0m0.071s |
0m0.071s |
0m0.070s |
0m0.070s |
0m0.070s |
多執行緒 |
0m0.049s |
0m0.049s |
0m0.049s |
0m0.049s |
0m0.049s |
0m0.049s |
單核(雙核機器禁掉一核),程序/執行緒數:255,列印次數10 |
||||||
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均 |
|
多程序 |
0m0.112s |
0m0.101s |
0m0.100s |
0m0.085s |
0m0.121s |
0m0.104s |
多執行緒 |
0m0.097s |
0m0.089s |
0m0.090s |
0m0.104s |
0m0.080s |
0m0.092s |
單核(雙核機器禁掉一核),程序/執行緒數:255,列印次數50 |
||||||
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均 |
|
多程序 |
0m0.459s |
0m0.531s |
0m0.499s |
0m0.499s |
0m0.524s |
0m0.507s |
多執行緒 |
0m0.387s |
0m0.456s |
0m0.435s |
0m0.423s |
0m0.408s |
0m0.422s |
單核(雙核機器禁掉一核),程序/執行緒數:255,列印次數100 |
||||||
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均 |
|
多程序 |
0m1.141s |
0m0.992s |
0m1.134s |
0m1.027s |
0m0.965s |
0m1.051s |
多執行緒 |
0m0.925s |
0m0.899s |
0m0.961s |
0m0.934s |
0m0.853s |
0m0.919s |
單核(雙核機器禁掉一核),程序/執行緒數:255,列印次數500 |
||||||
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均 |
|
多程序 |
0m5.221s |
0m5.258s |
0m5.706s |
0m5.288s |
0m5.455s |
0m5.334s |
多執行緒 |
0m4.689s |
0m4.578s |
0m4.670s |
0m4.566s |
0m4.722s |
0m4.646s |
單核(雙核機器禁掉一核),程序/執行緒數:255,列印次數1000 |
||||||
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均 |
|
多程序 |
0m12.680s |
0m16.555s |
0m11.158s |
0m10.922s |
0m11.206s |
0m11.681s |
多執行緒 |
0m12.993s |
0m13.087s |
0m13.082s |
0m13.485s |
0m13.053s |
0m13.074s |
單核(雙核機器禁掉一核),程序/執行緒數:255,列印次數5000 |
||||||
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均 |
|
多程序 |
1m27.348s |
1m5.569s |
0m57.275s |
1m5.029s |
1m15.174s |
1m8.591s |
多執行緒 |
1m25.813s |
1m29.299s |
1m23.842s |
1m18.914s |
1m34.872s |
1m26.318s |
單核(雙核機器禁掉一核),程序/執行緒數:255,列印次數10000 |
||||||
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均 |
|
多程序 |
2m8.336s |
2m22.999s |
2m11.046s |
2m30.040s |
2m5.752s |
2m14.137s |
多執行緒 |
2m46.666s |
2m44.757s |
2m34.528s |
2m15.018s |
2m41.436s |
2m40.240s |
本輪實驗是為了和周麗論文作對比,因此將程序/執行緒數量限制在255個,論文也是測試了255個程序/執行緒分別進行5次,10 次,50 次,100 次,500 次……10000 次列印的用時,論文得出的結果是:任務量較大時,多程序比多執行緒效率高;而完成的任務量較小時,多執行緒比多程序要快,重複列印 600 次時,多程序與多執行緒所耗費的時間相同。
雖然我的實驗直到1000列印次數時,多程序才開始領先,但考慮到使用的是NPTL執行緒庫的緣故,從而可以證實了論文的觀點。從我的實驗資料看,多執行緒和多程序兩組資料非常接近,考慮到資料的提取具有瞬間性,因此可以認為他們的速度是相同的。
是不是可以得出這樣的結論:多執行緒建立、銷燬速度快,而多執行緒切換速度快,這個結論我們會在第二個試驗中繼續試圖驗證
當前的網路環境中,我們更看中高併發、高負荷下的效能,縱觀前面的實驗步驟,最長的實驗週期不過2分鐘多一點,因此下面的實驗將向兩個方向延伸,第一,增加併發數量,第二,增加每程序/執行緒的工作強度。
二、增加併發數量的實驗
下面的實驗列印次數不變,而程序/執行緒數量逐漸增加。在實驗過程中多執行緒程式在後四組(執行緒數350,500,800,1000)的測試中都出現了“段錯誤”,出現錯誤的原因和多執行緒預分配執行緒棧有關。
實驗中的計算機CPU是32位,定址最大範圍是4GB(2的32次方),Linux是按照3GB/1GB的方式來分配記憶體,其中1GB屬於所有程序共享的核心空間,3GB屬於使用者空間(程序虛擬記憶體空間)。Linux2.6的預設執行緒棧大小是8M(通過ulimit -a檢視),對於多執行緒,在建立執行緒的時候系統會為每一個執行緒預分配執行緒棧地址空間,也就是8M的虛擬記憶體空間。執行緒數量太多時,執行緒棧累計的大小將超過程序虛擬記憶體空間大小(計算時需要排除程式文字、資料、共享庫等佔用的空間),這就是實驗中出現的“段錯誤”的原因。
Linux2.6的預設執行緒棧大小可以通過 ulimit -s 命令檢視或修改,我們可以計算出執行緒數的最大上線: (1024*1024*1024*3) / (1024*1024*8) = 384,實際數字應該略小與384,因為還要計算程式文字、資料、共享庫等佔用的空間。在當今的稍顯繁忙的WEB伺服器上,突破384的併發訪問並不是稀 罕的事情,要繼續下面的實驗需要將預設執行緒棧的大小減小,但這樣做有一定的風險,比如執行緒中的函式分配了大量的自動變數或者函式涉及很深的棧幀(典型的是 遞迴呼叫),執行緒棧就可能不夠用了。可以配合使用POSIX.1規定的兩個執行緒屬性guardsize和stackaddr來解決執行緒棧溢位問 題,guardsize控制著執行緒棧末尾之後的一篇記憶體區域,一旦執行緒棧在使用中溢位併到達了這片記憶體,程式可以捕獲系統核心發出的告警訊號,然後使用 malloc獲取另外的記憶體,並通過stackaddr改變執行緒棧的位置,以獲得額外的棧空間,這個動態擴充套件棧空間辦法需要手工程式設計,而且非常麻煩。
有兩種方法可以改變執行緒棧的大小,使用 ulimit -s 命令改變系統預設執行緒棧的大小,或者在程式碼中建立執行緒時通過pthread_attr_setstacksize函式改變棧尺寸,在實驗中使用的是第一種,在程式執行前先執行ulimit指令將預設執行緒棧大小改為1M:
ulimit -s 1024
time ./thread
單核(雙核機器禁掉一核),程序/執行緒數:100 ,列印次數1000 |
||||||
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均 |
|
多程序 |
0m3.834s |
0m3.759s |
0m4.376s |
0m3.936s |
0m3.851s |
0m3.874 |
多執行緒 |
0m3.646s |
0m4.498s |
0m4.219s |
0m3.893s |
0m3.943s |
0m4.018 |
單核(雙核機器禁掉一核),程序/執行緒數:255 ,列印次數1000 |
||||||
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均 |
|
多程序 |
0m9.731s |
0m9.833s |
0m10.046s |
0m9.830s |
0m9.866s |
0m9.843s |
多執行緒 |
0m9.961s |
0m9.699s |
0m9.958s |
0m10.111s |
0m9.686s |
0m9.873s |
單核(雙核機器禁掉一核),程序/執行緒數:350 ,列印次數1000 |
||||||
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均 |
|
多程序 |
0m13.773s |
0m13.500s |
0m13.519s |
0m13.474s |
0m13.351s |
0m13.498 |
多執行緒 |
0m12.754s |
0m13.251s |
0m12.813s |
0m16.861s |
0m12.764s |
0m12.943 |
單核(雙核機器禁掉一核),程序/執行緒數: 500 ,列印次數1000 |
||||||
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均 |
|
多程序 |
0m23.762s |
0m22.151s |
0m23.926s |
0m21.327s |
0m21.429s |
0m22.413 |
多執行緒 |
0m20.603s |
0m20.291s |
0m21.654s |
0m20.684s |
0m20.671s |
0m20.653 |
單核(雙核機器禁掉一核),程序/執行緒數:800 ,列印次數1000 |
||||||
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均 |
|
多程序 |
0m33.616s |
0m31.757s |
0m31.759s |
0m32.232s |
0m32.498s |
0m32.163 |
多執行緒 |
0m32.050s |
0m32.787s |
0m33.055s |
0m32.902s |
0m32.235s |
0m32.641 |
單核(雙核機器禁掉一核),程序/執行緒數: 1000 ,列印次數1000 |
||||||
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均 |
|
多程序 |
0m40.301s |
0m41.083s |
0m41.634s |
0m40.247s |
0m40.717s |
0m40.700 |
多執行緒 |
0m41.633s |
0m41.118s |
0m42.700s |
0m42.134s |
0m41.170s |
0m41.646 |
【實驗結論】
當執行緒/程序逐漸增多時,執行相同任務時,執行緒所花費時間相對於程序有下降的趨勢(本人懷疑後兩組資料受系統其他瓶頸的影響),這是不是進一步驗證了多執行緒建立、銷燬速度快,而多程序切換速度快。
出現了執行緒棧的問題,讓我特別關心Java執行緒是怎樣處理的,因此用Java語言寫了同樣的實驗程式,Java程式載入虛擬機器環境比較耗時,所以沒 有用time提取測試時間,而直接將測時寫入程式碼。對Linux上的C程式設計不熟悉的Java程式設計師也可以用這個程式去對比理解上面的C語言試驗程式。
- import java.io.File;
- import java.io.FileNotFoundException;
- import java.io.FileOutputStream;
- import java.io.IOException;
- public class MyThread extends Thread
- {
- static int P_NUMBER = 1000; /* 併發執行緒數量 */
- static int COUNT = 1000; /* 每執行緒列印字串次數 */
- static String s = "hello linux\n";
- static FileOutputStream out = null; /* 檔案輸出流 */
- @Override
- public void run()
- {
- for (int i = 0; i < COUNT; i++)
- {
- System.out.printf("[%d]%s", i, s); /* 向控制檯輸出 */
- StringBuilder sb = new StringBuilder(16);
- sb.append("[").append(i).append("]").append(s);
- try
- {
- out.write(sb.toString().getBytes());/* 向日志文件輸出 */
- }
- catch (IOException e)
- {
- e.printStackTrace();
- }
- }
- }
- public static void main(String[] args) throws FileNotFoundException, InterruptedException
- {
- MyThread[] threads = new MyThread[P_NUMBER]; /* 執行緒陣列 */
- File file = new File("Javalogfile.log");
- out = new FileOutputStream(file, true); /* 日誌檔案輸出流 */
- System.out.println("開始執行");
- long start = System.currentTimeMillis();
- for (int i = 0; i < P_NUMBER; i++) //建立執行緒
- {
- threads[i] = new MyThread();
- threads[i].start();
- }
- for (int i = 0; i < P_NUMBER; i++) //回收執行緒
- {
- threads[i].join();
- }
- System.out.println("用時:" + (System.currentTimeMillis() – start) + " 毫秒");
- return;
- }
- }
程序/執行緒數:1000 ,列印次數1000(用得原作者的資料) |
||||||
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均 |
|
多執行緒 |
65664 ms |
66269 ms |
65546ms |
65931ms |
66540ms |
65990 ms |
Java程式比C程式慢一些在情理之中,但Java程式並沒有出現執行緒棧問題,5次測試都平穩完成,可以用下面的ps指令獲得java程序中執行緒的數量:
[email protected]:~$ ps -eLf | grep MyThread | wc -l
1010
用ps測試執行緒數在1010上維持了很長時間,多出的10個執行緒應該是jvm內部的管理執行緒,比如用於GC。我不知道Java建立執行緒時預設棧的大 小是多少,很多資料說法不統一,於是下載了Java的原始碼jdk-6u21-fcs-src-b07-jrl-17_jul_2010.jar(實驗環境 安裝的是 SUN jdk 1.6.0_20-b02),但沒能從中找到需要的資訊。對於jvm的執行,java提供了控制引數,因此再次測試時,通過下面的引數將Java執行緒棧大 小定義在8192k,和Linux的預設大小一致:
[email protected]:~/tmp1$ java -Xss8192k MyThread
出乎意料的是並沒有出現想象中的異常,但用ps偵測執行緒數最高到達337,我判斷程式在建立執行緒時在棧到達可用記憶體的上線時就停止繼續建立了,程式執行的時間遠小於估計值也證明了這個判斷。程式雖然沒有丟擲異常,但執行的並不正常,另一個問題是最後並沒有打印出“用時 xxx毫秒”資訊。
這次測試更加深了我的一個長期的猜測:Java的Web容器不穩定。因為我是多年編寫B/S的Java程式設計師,WEB服務不穩定常常掛掉也是司空見慣的,除了自己或專案組成員水平不高,程式碼編寫太爛的原因之外,我一直猜測還有更深層的原因,如果就是執行緒原因的話,這顛覆性可比本篇文章的多程序效能顛覆性要大得多,想想世界上有多少Tomcat、Jboss、Websphere、weblogic在跑著,嘿嘿。
這次測試還打破了以前的一個說法:單CPU上併發超過6、7百,執行緒或程序間的切換就會佔用大量CPU時間,造成伺服器效率會急劇下降。但從上面的實驗來看,程序/執行緒數到1000時(這差不多是非常繁忙的WEB伺服器了),仍具有很好的線性。
三、增加每程序/執行緒的工作強度的實驗
這次將程式列印資料增大,原來列印字串為:
-
char *s = "hello linux\0";
現在修改為每次列印256個位元組資料:
- char *s = "1234567890abcdef\
- 1234567890abcdef\
- 1234567890abcdef\
- 1234567890abcdef\
- 1234567890abcdef\
- 1234567890abcdef\
- 1234567890abcdef\
- 1234567890abcdef\
- 1234567890abcdef\
- 1234567890abcdef\
- 1234567890abcdef\
- 1234567890abcdef\
- 1234567890abcdef\
- 1234567890abcdef\
- 1234567890abcdef\
- 1234567890abcdef\0";
單核(雙核機器禁掉一核),程序/執行緒數:255 ,列印次數100 |
||||||
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均 |
|
多程序 |
0m6.977s |
0m7.358s |
0m7.520s |
0m7.282s |
0m7.218s |
0m7.286 |
多執行緒 |
0m7.035s |
0m7.552s |
0m7.087s |
0m7.427s |
0m7.257s |
0m7.257 |
單核(雙核機器禁掉一核),程序/執行緒數: 255,列印次數500 |
||||||
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均 |
|
多程序 |
0m35.666s |
0m36.009s |
0m36.532s |
0m35.578s |
0m41.537s |
0m36.069 |
多執行緒 |
0m37.290s |
0m35.688s |
0m36.377s |
0m36.693s |
0m36.784s |
0m36.618 |
單核(雙核機器禁掉一核),程序/執行緒數: 255,列印次數1000 |
||||||
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均 |
|
多程序 |
1m8.864s |
1m11.056s |
1m10.273s |
1m12.317s |
1m20.193s |
1m11.215 |
多執行緒 |
1m11.949s |
1m13.088s |
1m12.291s |
1m9.677s |
1m12.210s |
1m12.150 |
【實驗結論】
從上面的實驗比對結果看,即使Linux2.6使用了新的NPTL執行緒庫(據說比原執行緒庫效能提高了很多,唉,又是據說!),多執行緒比較多程序在效率上沒有任何的優勢,線上程數增大時多執行緒程式還出現了執行錯誤,實驗可以得出下面的結論:
在Linux2.6上,多執行緒並不比多程序速度快,考慮到執行緒棧的問題,多程序在併發上有優勢。
四、多程序和多執行緒在建立和銷燬上的效率比較
預先建立程序或執行緒可以節省程序或執行緒的建立、銷燬時間,在實際的應用中很多程式使用了這樣的策略,比如Apapche預先建立程序、Tomcat 預先建立執行緒,通常叫做程序池或執行緒池。在大部分人的概念中,程序或執行緒的建立、銷燬是比較耗時的,在stevesn的著作《Unix網路程式設計》中有這樣 的對比圖(第一卷 第三版 30章 客戶/伺服器程式設計正規化):
行號 | 伺服器描述 | 程序控制CPU時間(秒,與基準之差) | ||
---|---|---|---|---|
Solaris2.5.1 | Digital Unix4.0b | BSD/OS3.0 | ||
0 | 迭代伺服器(基準測試,無程序控制) | 0.0 | 0.0 | 0.0 |
1 | 簡單併發服務,為每個客戶請求fork一個程序 | 504.2 | 168.9 | 29.6 |
2 | 預先派生子程序,每個子程序呼叫accept | 6.2 | 1.8 | |
3 | 預先派生子程序,用檔案鎖保護accept | 25.2 | 10.0 | 2.7 |
4 | 預先派生子程序,用執行緒互斥鎖保護accept | 21.5 | ||
5 | 預先派生子程序,由父程序向子程序傳遞套接字 | 36.7 | 10.9 | 6.1 |
6 | 併發服務,為每個客戶請求建立一個執行緒 | 18.7 | 4.7 | |
7 | 預先建立執行緒,用互斥鎖保護accept | 8.6 | 3.5 | |
8 | 預先建立執行緒,由主執行緒呼叫accept | 14.5 | 5.0 |
stevens已駕鶴西去多年,但《Unix網路程式設計》一書仍具有巨大的影響力,上表中stevens比較了三種伺服器上多程序和多執行緒的執行效 率,因為三種伺服器所用計算機不同,表中資料只能縱向比較,而橫向無可比性,stevens在書中提供了這些測試程式的原始碼(也可以在網上下載)。書中介 紹了測試環境,兩臺與伺服器處於同一子網的客戶機,每個客戶併發5個程序(伺服器同一時間最多10個連線),每個客戶請求從伺服器獲取4000位元組資料, 預先派生子程序或執行緒的數量是15個。
第0行是迭代模式的基準測試程式,伺服器程式只有一個程序在執行(同一時間只能處理一個客戶請求),因為沒有程序或執行緒的排程切換,因此它的速度是 最快的,表中其他服務模式的執行數值是比迭代模式多出的差值。迭代模式很少用到,在現有的網際網路服務中,DNS、NTP服務有它的影子。第1~5行是多進 程服務模式,期中第1行使用現場fork子程序,2~5行都是預先建立15個子程序模式,在多程序程式中套接字傳遞不太容易(相對於多線 程),stevens在這裡提供了4個不同的處理accept的方法。6~8行是多執行緒服務模式,第6行是現場為客戶請求建立子執行緒,7~8行是預先建立 15個執行緒。表中有的格子是空白的,是因為這個系統不支援此種模式,比如當年的BSD不支援執行緒,因此BSD上多執行緒的資料都是空白的。
從資料的比對看,現場為每客戶fork一個程序的方式是最慢的,差不多有20倍的速度差異,Solaris上的現場fork和預先建立子程序的最大差別是504.2 :21.5,但我們不能理解為預先建立模式比現場fork快20倍,原因有兩個:
1. stevens的測試已是十幾年前的了,現在的OS和CPU已起了翻天覆地的變化,表中的數值需要重新測試。
2. stevens沒有提供伺服器程式整體的執行計時,我們無法理解504.2 :21.5的實際執行效率,有可能是1504.2 : 1021.5,也可能是100503.2 : 100021.5,20倍的差異可能很大,也可能可以忽略。
因此我寫了下面的實驗程式,來計算在Linux2.6上建立、銷燬10萬個程序/執行緒的絕對用時。
建立10萬個程序(forkcreat.c):
- #include <stdio.h>
- #include <signal.h>
- #include <stdio.h>
- #include <unistd.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- #include <sys/types.h>
- #include <sys/wait.h>
- int count;//子程序建立成功數量
- int fcount;//子程序建立失敗數量
- int scount;//子程序回收數量
- /*訊號處理函式–子程序關閉收集*/
- void sig_chld(int signo)
- {
- pid_t chldpid;//子程序id
- int stat;//子程序的終止狀態
- //子程序回收,避免出現殭屍程序
- while((chldpid=wait(&stat)>0))
- {
- scount++;
- }
- }
- int main()
- {
- //註冊子程序回收訊號處理函式
- signal(SIGCHLD,sig_chld);
- int i;
- for(i=0;i<100000;i++)//fork()10萬個子程序
- {
- pid_t pid=fork();
- if(pid==-1)//子程序建立失敗
- {
- fcount++;
- }
- else if(pid>0)//子程序建立成功
- {
- count++;
- }
- else if(pid==0)//子程序執行過程
- {
- exit(0);
- }
- }
- printf("count:%d fount:%d scount:%d\n",count,fcount,scount);
- }
建立10萬個執行緒(pthreadcreat.c):
- #include <stdio.h>
- #include <pthread.h>
- int count=0;//成功建立執行緒數量
- void thread(void)
- {
- //啥也不做
- }
- int main(void)
- {
- pthread_t id;//執行緒id
- int i,ret;
- for(i=0;i<100000;i++)//建立10萬個執行緒
- {
- ret=pthread_create(&id,NULL,(void *)thread,NULL);
- if(ret!=0)
- {
- printf("Create pthread error!\n");
- return(1);
- }
- count++;
- pthread_join(id,NULL);
- }
- printf("count:%d\n",count);
- }
建立10萬個執行緒的Java程式:
- public class ThreadTest
- {
- public static void main(String[] ags) throws InterruptedException
- {
- System.out.println("開始執行");
- long start = System.currentTimeMillis();
- for(int i = 0; i < 100000; i++) //建立10萬個執行緒
- {
- Thread athread = new Thread(); //建立執行緒物件
- athread.start(); //啟動執行緒
- athread.join(); //等待該執行緒停止
- }
- System.out.println("用時:" + (System.currentTimeMillis() – start) + " 毫秒");
- }
- }
單核(雙核機器禁掉一核),建立銷燬10萬個程序/執行緒 |
||||||
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均 |
|
多程序 |
0m8.774s |
0 |