魚還是熊掌:淺談多進程多線程的選擇
關於多進程和多線程,教科書上最經典的一句話是“進程是資源分配的最小單位,線程是CPU調度的最小單位”,這句話應付考試基本上夠了,但如果在工作中遇到類似的選擇問題,那就沒有這麼簡單了,選的不好,會讓你深受其害。
經常在網絡上看到有的XDJM問“多進程好還是多線程好?”、“Linux下用多進程還是多線程?”等等期望一勞永逸的問題,我只能說:沒有最好,只有更好。根據實際情況來判斷,哪個更加合適就是哪個好。
我們按照多個不同的維度,來看看多線程和多進程的對比(注:因爲是感性的比較,因此都是相對的,不是說一個好得不得了,另外一個差的無法忍受)。
對比維度 |
多進程 |
多線程 |
總結 |
數據共享、同步 |
數據共享複雜,需要用IPC;數據是分開的,同步簡單 |
因爲共享進程數據,數據共享簡單,但也是因爲這個原因導致同步複雜 |
各有優勢 |
內存、CPU |
佔用內存多,切換複雜,CPU利用率低 |
佔用內存少,切換簡單,CPU利用率高 |
線程佔優 |
創建銷燬、切換 |
創建銷燬、切換複雜,速度慢 |
創建銷燬、切換簡單,速度很快 |
線程佔優 |
編程、調試 |
編程簡單,調試簡單 |
編程複雜,調試複雜 |
進程佔優 |
可靠性 |
進程間不會互相影響 |
一個線程掛掉將導致整個進程掛掉 |
進程佔優 |
分佈式 |
適應於多核、多機分佈式;如果一臺機器不夠,擴展到多臺機器比較簡單 |
適應於多核分佈式 |
進程佔優 |
1)需要頻繁創建銷燬的優先用線程
原因請看上面的對比。
這種原則最常見的應用就是Web服務器了,來一個連接建立一個線程,斷了就銷燬線程,要是用進程,創建和銷燬的代價是很難承受的
2)需要進行大量計算的優先使用線程
所謂大量計算,當然就是要耗費很多CPU,切換頻繁了,這種情況下線程是最合適的。
這種原則最常見的是圖像處理、算法處理。
3)強相關的處理用線程,弱相關的處理用進程
什麼叫強相關、弱相關?理論上很難定義,給個簡單的例子就明白了。
一般的Server需要完成如下任務:消息收發、消息處理。“消息收發”和“消息處理”就是弱相關的任務,而“消息處理”裏面可能又分爲“消息解碼”、“業務處理”,這兩個任務相對來說相關性就要強多了。因此“消息收發”和“消息處理”可以分進程設計,“消息解碼”、“業務處理”可以分線程設計。
當然這種劃分方式不是一成不變的,也可以根據實際情況進行調整。
4)可能要擴展到多機分佈的用進程,多核分佈的用線程
原因請看上面對比。
5)都滿足需求的情況下,用你最熟悉、最拿手的方式
至於“數據共享、同步”、“編程、調試”、“可靠性”這幾個維度的所謂的“複雜、簡單”應該怎麼取捨,我只能說:沒有明確的選擇方法。但我可以告訴你一個選擇原則:如果多進程和多線程都能夠滿足要求,那麼選擇你最熟悉、最拿手的那個。
需要提醒的是:雖然我給了這麼多的選擇原則,但實際應用中基本上都是“進程+線程”的結合方式,千萬不要真的陷入一種非此即彼的誤區。
消耗資源:
從內核的觀點看,進程的目的就是擔當分配系統資源(CPU時間、內存等)的基本單位。線程是進程的一個執行流,是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位。
線程,它們彼此之間使用相同的地址空間,共享大部分數據,啓動一個線程所花費的空間遠遠小於啓動一個進程所花費的空間,而且,線程間彼此切換所需的時間也遠遠小於進程間切換所需要的時間。據統計,總的說來,一個進程的開銷大約是一個線程開銷的30倍左右,當然,在具體的系統上,這個數據可能會有較大的區別。
通訊方式:
進程之間傳遞數據只能是通過通訊的方式,即費時又不方便。線程時間數據大部分共享(線程函數內部不共享),快捷方便。但是數據同步需要鎖對於static變量尤其注意
線程自身優勢:
提高應用程序響應;使多CPU系統更加有效。操作系統會保證當線程數不大於CPU數目時,不同的線程運行於不同的CPU上;
改善程序結構。一個既長又複雜的進程可以考慮分爲多個線程,成爲幾個獨立或半獨立的運行部分,這樣的程序會利於理解和修改。
實驗數據:
進程實驗代碼(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 |
出的結果是:任務量較大時,多進程比多線程效率高;而完成的任務量較小時,多線程比多進程要快,重複打印 600 次時,多進程與多線程所耗費的時間相同。
、增加每進程/線程的工作強度的實驗
這次將程序打印數據增大,原來打印字符串爲:
- 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 |
0m8.780s |
0m8.475s |
0m8.592s |
0m8.687s |
0m8.684 |
多線程 |
0m0.663s |
0m0.660s |
0m0.662s |
0m0.660s |
0m0.661s |
0m0.661 |
創建銷燬10萬個線程(Java) |
---|
12286毫秒 |
從數據可以看出,多線程比多進程在效率上有10多倍的優勢,但不能讓我們在使用哪種併發模式上定性,這讓我想起多年前政治課上的一個場景:在講到優越性時,面對着幾個對此發表質疑評論的調皮男生,我們的政治老師發表了高見,“不能只橫向地和當今的發達國家比,你應該縱向地和過去中國幾十年的發展歷史 比”。政治老師的話套用在當前簡直就是真理,我們看看,即使是在賽揚CPU上,創建、銷燬進程/線程的速度都是空前的,可以說是有質的飛躍的,平均創建銷燬一個進程的速度是0.18毫秒,對於當前服務器幾百、幾千的併發量,還有預先派生子進程/線程的必要嗎?
預先派生子進程/線程比現場創建子進程/線程要複雜很多,不僅要對池中進程/線程數量進行動態管理,還要解決多進程/多線程對accept的“搶” 問題,在stevens的測試程序中,使用了“驚羣”和“鎖”技術。即使stevens的數據表格中,預先派生線程也不見得比現場創建線程快,在 《Unix網絡編程》第三版中,新作者參照stevens的測試也提供了一組數據,在這組數據中,現場創建線程模式比預先派生線程模式已有了效率上的優勢。因此我對這一節實驗下的結論是:
預先派生進程/線程的模式(進程池、線程池)技術,不僅複雜,在效率上也無優勢,在新的應用中可以放心大膽地爲客戶連接請求去現場創建進程和線程。
我想,這是fork迷們最願意看到的結論了。
五、雙核系統重複周麗論文實驗步驟
雙核,進程/線程數:255 ,打印次數10 |
||||||
|
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均(單核倍數) |
多進程 |
0m0.061s |
0m0.053s |
0m0.068s |
0m0.061s |
0m0.059s |
0m0.060(1.73) |
多線程 |
0m0.054s |
0m0.040s |
0m0.053s |
0m0.056s |
0m0.042s |
0m0.050(1.84) |
雙核,進程/線程數: 255,打印次數100 |
||||||
|
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均(單核倍數) |
多進程 |
0m0.918s |
0m1.198s |
0m1.241s |
0m1.017s |
0m1.172s |
0m1.129(0.93) |
多線程 |
0m0.897s |
0m1.166s |
0m1.091s |
0m1.360s |
0m0.997s |
0m1.085(0.85) |
雙核,進程/線程數: 255,打印次數1000 |
||||||
|
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均(單核倍數) |
多進程 |
0m11.276s |
0m11.269s |
0m11.218s |
0m10.919s |
0m11.201s |
0m11.229(1.04) |
多線程 |
0m11.525s |
0m11.984s |
0m11.715s |
0m11.433s |
0m10.966s |
0m11.558(1.13) |
雙核,進程/線程數:255 ,打印次數10000 |
||||||
|
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均(單核倍數) |
多進程 |
1m54.328s |
1m54.748s |
1m54.807s |
1m55.950s |
1m57.655s |
1m55.168(1.16) |
多線程 |
2m3.021s |
1m57.611s |
1m59.139s |
1m58.297s |
1m57.258s |
1m58.349(1.35) |
【實驗結論】
雙核處理器在完成任務量較少時,沒有系統其他瓶頸因素影響時基本上是單核的兩倍,在任務量較多時,受系統其他瓶頸因素的影響,速度明顯趨近於單核的速度。
六、併發服務的不可測性
看到這裏,你會感覺到我有挺進程、貶線程的論調,實際上對於現實中的併發服務具有不可測性,前面的實驗和結論只可做參考,而不可定性。對於不可測性,我舉個生活中的例子。
這幾年在大都市生活的朋友都感覺城市交通狀況越來越差,到處堵車,從好的方面想這不正反應了我國GDP的高速發展。如果你7、8年前來到西安市,穿 過南二環上的一些十字路口時,會發現一個奇怪的U型彎的交通管制,爲了更好的說明,我畫了兩張圖來說明,第一張圖是採用U型彎之前的,第二張是採用U型彎 之後的。
南二環交通圖一
南二環交通圖二
爲了講述的方便,我們不考慮十字路口左拐的情況,在圖一中東西向和南北向的車輛交匯在十字路口,用紅綠燈控制同一時間只能東西向或南北向通行,一般 的十字路口都是這樣管控的。隨着車輛的增多,十字路口的堵塞越來越嚴重,尤其是上下班時間經常出現堵死現象。於是交通部門在不動用過多經費的情況下而採用 了圖二的交通管制,東西向車輛行進方式不變,而南北向車輛不能直行,需要右拐到下一個路口拐一個超大的U型彎,這樣的措施避免了因車輛交錯而引發堵死的次 數,從而提高了車輛的通過效率。我曾經問一個每天上下班乘公交經過此路口的同事,他說這樣的改動不一定每次上下班時間都能縮短,但上班時間有保障了,從而 遲到次數減少了。如果今天你去西安市的南二環已經見不到U型彎了,東西向建設了高架橋,車輛分流後下層的十字路口已恢復爲圖一方式。
從效率的角度分析,在圖一中等一個紅燈45秒,遠遠小於圖二拐那個U型彎用去的時間,但實際情況正好相反。我們可以設想一下,如果路上的所有運行車 輛都是同一型號(比如說全是QQ3微型車),所有的司機都遵守交規,具有同樣的心情和性格,那麼圖一的通行效率肯定比圖二高。現實中就不一樣了,首先車輛 不統一,有大車、小車、快車、慢車,其次司機的品行不一,有特別遵守交規的,有想耍點小聰明的,有性子慢的,也有的性子急,時不時還有三輪摩托逆行一下, 十字路口的“死鎖”也就難免了。
那麼在什麼情況下圖二優於圖一,是否能拿出一個科學分析數據來呢?以現在的科學技術水平是拿不出來的,就像長期的天氣預報不可預測一樣,西安市的交管部門肯定不是分析各種車輛的運行規律、速度,再進行復雜的社會學、心理學分析做出U型彎的決定的,這就是要說的不可測性。
現實中的程序亦然如此,比如WEB服務器,有的客戶在快車道(寬帶),有的在慢車道(窄帶),有的性子慢(等待半分鐘也無所謂),有的性子急(拼命 的進行瀏覽器刷新),時不時還有一兩個黑客混入其中,這種情況每個服務器都不一樣,既是是同一服務器每時每刻的變化也不一樣,因此說不具有可測性。開發者 和維護者能做的,不論是前面的這種實驗測試,還是對具體網站進行的壓力測試,最多也就能模擬相當於QQ3通過十字路口的場景。
結束語
本篇文章比較了Linux系統上多線程和多進程的運行效率,在實際應用時還有其他因素的影響,比如網絡通訊時採用長連接還是短連接,是否採用 select、poll,java中稱爲nio的機制,還有使用的編程語言,例如Java不能使用多進程,PHP不能使用多線程,這些都可能影響到併發模 式的選型。
最後還有兩點提醒:
1. 文章中的所有實驗數據有環境約束。
2. 由於並行服務的不可測性,文章中的觀點應該只做參考,而不要去定性。