多線程和多進程的區別(C++)


很想寫點關於多進程和多線程的東西,我確實很愛他們。但是每每想動手寫點關於他們的東西,卻總是求全心理作祟,始終動不了手。

今天終於下了決心,寫點東西,以后可以再修修補補也無妨。

 

一.為何需要多進程(或者多線程),為何需要並發?

這個問題或許本身都不是個問題。但是對於沒有接觸過多進程編程的朋友來說,他們確實無法感受到並發的魅力以及必要性。

我想,只要你不是整天都寫那種int main()到底的代碼的人,那么或多或少你會遇到代碼響應不夠用的情況,也應該有嘗過並發編程的甜頭。就像一個快餐點的服務員,既要在前台接待客戶點餐,又要接電話送外賣,沒有分身術肯定會忙得你焦頭爛額的。幸運的是確實有這么一種技術,讓你可以像孫悟空一樣分身,靈魂出竅,樂哉樂哉地輕松應付一切狀況,這就是多進程/線程技術。

並發技術,就是可以讓你在同一時間同時執行多條任務的技術。你的代碼將不僅僅是從上到下,從左到右這樣規規矩矩的一條線執行。你可以一條線在main函數里跟你的客戶交流,另一條線,你早就把你外賣送到了其他客戶的手里。

 

所以,為何需要並發?因為我們需要更強大的功能,提供更多的服務,所以並發,必不可少。

 

二.多進程

什么是進程。最直觀的就是一個個pid,官方的說法就:進程是程序在計算機上的一次執行活動。

說得簡單點,下面這段代碼執行的時候

[cpp]  view plain copy
 
  1. int main()  
  2.   
  3. {  
  4.   
  5. printf(”pid is %d/n”,getpid() );  
  6.   
  7. return 0;  
  8.   
  9. }  

 

進入main函數,這就是一個進程,進程pid會打印出來,然后運行到return,該函數就退出,然后由於該函數是該進程的唯一的一次執行,所以return后,該進程也會退出。

 

看看多進程。linux下創建子進程的調用是fork();

 

  

[cpp]  view plain copy
 
  1. #include <unistd.h>  
  2. #include <sys/types.h>   
  3. #include <stdio.h>  
  4.   
  5.    
  6.   
  7. void print_exit()  
  8. {  
  9.        printf("the exit pid:%d/n",getpid() );  
  10. }  
  11.   
  12. main ()   
  13. {   
  14.    pid_t pid;   
  15.    atexit( print_exit );      //注冊該進程退出時的回調函數  
  16.       pid=fork();   
  17.         if (pid < 0)   
  18.                 printf("error in fork!");   
  19.         else if (pid == 0)   
  20.                 printf("i am the child process, my process id is %d/n",getpid());   
  21.         else   
  22.         {  
  23.                printf("i am the parent process, my process id is %d/n",getpid());   
  24.               sleep(2);  
  25.               wait();  
  26.        }  
  27.   
  28. }  


 

i am the child process, my process id is 15806
the exit pid:15806
i am the parent process, my process id is 15805
the exit pid:15805

這是gcc測試下的運行結果。

 

關於fork函數,功能就是產生子進程,由於前面說過,進程就是執行的流程活動。

那么fork產生子進程的表現就是它會返回2次,一次返回0,順序執行下面的代碼。這是子進程。

一次返回子進程的pid,也順序執行下面的代碼,這是父進程。

(為何父進程需要獲取子進程的pid呢?這個有很多原因,其中一個原因:看最后的wait,就知道父進程等待子進程的終結后,處理其task_struct結構,否則會產生僵屍進程,扯遠了,有興趣可以自己google)。

如果fork失敗,會返回-1.

額外說下atexit( print_exit ); 需要的參數肯定是函數的調用地址。

這里的print_exit 是函數名還是函數指針呢?答案是函數指針,函數名永遠都只是一串無用的字符串。

某本書上的規則:函數名在用於非函數調用的時候,都等效於函數指針。

 

說到子進程只是一個額外的流程,那他跟父進程的聯系和區別是什么呢?

我很想建議你看看linux內核的注解(有興趣可以看看,那里才有本質上的了解),總之,fork后,子進程會復制父進程的task_struct結構,並為子進程的堆棧分配物理頁。理論上來說,子進程應該完整地復制父進程的堆,棧以及數據空間,但是2者共享正文段。

關於寫時復制:由於一般 fork后面都接着exec,所以,現在的 fork都在用寫時復制的技術,顧名思意,就是,數據段,堆,棧,一開始並不復制,由父,子進程共享,並將這些內存設置為只讀。直到父,子進程一方嘗試寫這些區域,則內核才為需要修改的那片內存拷貝副本。這樣做可以提高 fork的效率。

 

三.多線程

線程是可執行代碼的可分派單元。這個名稱來源於“執行的線索”的概念。在基於線程的多任務的環境中,所有進程有至少一個線程,但是它們可以具有多個任務。這意味着單個程序可以並發執行兩個或者多個任務。

 

簡而言之,線程就是把一個進程分為很多片,每一片都可以是一個獨立的流程。這已經明顯不同於多進程了,進程是一個拷貝的流程,而線程只是把一條河流截成很多條小溪。它沒有拷貝這些額外的開銷,但是僅僅是現存的一條河流,就被多線程技術幾乎無開銷地轉成很多條小流程,它的偉大就在於它少之又少的系統開銷。(當然偉大的后面又引發了重入性等種種問題,這個后面慢慢比較)。

還是先看linux提供的多線程的系統調用:

 

int pthread_create(pthread_t *restrict tidp,
                   const pthread_attr_t *restrict attr,
                   void *(*start_rtn)(void), 
                   void *restrict arg);

Returns: 0 if OK, error number on failure

第一個參數為指向線程標識符的指針。
第二個參數用來設置線程屬性。
第三個參數是線程運行函數的起始地址。
最后一個參數是運行函數的參數。

   

[cpp]  view plain copy
 
  1. #include<stdio.h>  
  2. #include<string.h>  
  3. #include<stdlib.h>  
  4. #include<unistd.h>  
  5. #include<pthread.h>  
  6.   
  7.    
  8. void* task1(void*);  
  9. void* task2(void*);  
  10.   
  11.   
  12. void usr();  
  13. int p1,p2;  
  14.   
  15. int main()  
  16. {  
  17.     usr();  
  18.     getchar();  
  19.     return 1;  
  20. }  
  21.   
  22.    
  23.   
  24. void usr()  
  25. {  
  26.        pthread_t pid1, pid2;  
  27.     pthread_attr_t attr;  
  28.        void *p;  
  29.         int ret=0;  
  30.        pthread_attr_init(&attr);         //初始化線程屬性結構  
  31.        pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);   //設置attr結構為分離  
  32.        pthread_create(&pid1, &attr, task1, NULL);         //創建線程,返回線程號給pid1,線程屬性設置為attr的屬性,線程函數入口為task1,參數為NULL  
  33.     pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);  
  34. pthread_create(&pid2, &attr, task2, NULL);  
  35. //前台工作  
  36.   
  37. ret=pthread_join(pid2, &p);         //等待pid2返回,返回值賦給p  
  38.        printf("after pthread2:ret=%d,p=%d/n", ret,(int)p);            
  39.   
  40. }  
  41.   
  42. void* task1(void *arg1)  
  43. {  
  44. printf("task1/n");  
  45. //艱苦而無法預料的工作,設置為分離線程,任其自生自滅  
  46.     pthread_exit( (void *)1);  
  47.   
  48. }  
  49.   
  50. void* task2(void *arg2)  
  51. {  
  52.     int i=0;  
  53.     printf("thread2 begin./n");  
  54.     //繼續送外賣的工作  
  55.     pthread_exit((void *)2);  
  56. }  


 

這個多線程的例子應該很明了了,主線程做自己的事情,生成2個子線程,task1為分離,任其自生自滅,而task2還是繼續送外賣,需要等待返回。(因該還記得前面說過僵屍進程吧,線程也是需要等待的。如果不想等待,就設置線程為分離線程)

 額外的說下,linux下要編譯使用線程的代碼,一定要記得調用pthread庫。如下編譯:

 gcc -o pthrea -pthread  pthrea.c

 

四.比較以及注意事項

 

1.看完前面,應該對多進程和多線程有個直觀的認識。如果總結多進程和多線程的區別,你肯定能說,前者開銷大,后者開銷較小。確實,這就是最基本的區別。

2.線程函數的可重入性:

說到函數的可重入,和線程安全,我偷懶了,引用網上的一些總結。

 

 

線程安全:概念比較直觀。一般說來,一個函數被稱為線程安全的,當且僅當被多個並發線程反復調用時,它會一直產生正確的結果。

  

 

 

 

 

可重入:概念基本沒有比較正式的完整解釋,但是它比線程安全要求更嚴格。根據經驗,所謂“重入”,常見的情況是,程序執行到某個函數foo()時,收到信號,於是暫停目前正在執行的函數,轉到信號處理函數,而這個信號處理函數的執行過程中,又恰恰也會進入到剛剛執行的函數foo(),這樣便發生了所謂的重入。此時如果foo()能夠正確的運行,而且處理完成后,之前暫停的foo()也能夠正確運行,則說明它是可重入的。

 

線程安全的條件:

 

要確保函數線程安全,主要需要考慮的是線程之間的共享變量。屬於同一進程的不同線程會共享進程內存空間中的全局區和堆,而私有的線程空間則主要包括棧和寄存器。因此,對於同一進程的不同線程來說,每個線程的局部變量都是私有的,而全局變量、局部靜態變量、分配於堆的變量都是共享的。在對這些共享變量進行訪問時,如果要保證線程安全,則必須通過加鎖的方式。

 

可重入的判斷條件:

 

 

要確保函數可重入,需滿足一下幾個條件:

1、不在函數內部使用靜態或全局數據 
2、不返回靜態或全局數據,所有數據都由函數的調用者提供。 
3、使用本地數據,或者通過制作全局數據的本地拷貝來保護全局數據。
4、不調用不可重入函數。

 

 

可重入與線程安全並不等同,一般說來,可重入的函數一定是線程安全的,但反過來不一定成立。它們的關系可用下圖來表示:

 

 

 

比如:strtok函數是既不可重入的,也不是線程安全的;加鎖的strtok不是可重入的,但線程安全;而strtok_r既是可重入的,也是線程安全的。

 

如果我們的線程函數不是線程安全的,那在多線程調用的情況下,可能導致的后果是顯而易見的——共享變量的值由於不同線程的訪問,可能發生不可預料的變化,進而導致程序的錯誤,甚至崩潰。

 

3.關於IPC(進程間通信)

由於多進程要並發協調工作,進程間的同步,通信是在所難免的。

稍微列舉一下linux常見的IPC.

linux下進程間通信的幾種主要手段簡介:

  1. 管道(Pipe)及有名管道(named pipe):管道可用於具有親緣關系進程間的通信,有名管道克服了管道沒有名字的限制,因此,除具有管道所具有的功能外,它還允許無親緣關系進程間的通信;
  2. 信號(Signal):信號是比較復雜的通信方式,用於通知接受進程有某種事件發生,除了用於進程間通信外,進程還可以發送信號給進程本身;linux除了支持Unix早期信號語義函數sigal外,還支持語義符合Posix.1標准的信號函數sigaction(實際上,該函數是基於BSD的,BSD為了實現可靠信號機制,又能夠統一對外接口,用sigaction函數重新實現了signal函數);
  3. 報文(Message)隊列(消息隊列):消息隊列是消息的鏈接表,包括Posix消息隊列system V消息隊列。有足夠權限的進程可以向隊列中添加消息,被賦予讀權限的進程則可以讀走隊列中的消息。消息隊列克服了信號承載信息量少,管道只能承載無格式字節流以及緩沖區大小受限等缺點。
  4. 共享內存:使得多個進程可以訪問同一塊內存空間,是最快的可用IPC形式。是針對其他通信機制運行效率較低而設計的。往往與其它通信機制,如信號量結合使用,來達到進程間的同步及互斥。
  5. 信號量(semaphore):主要作為進程間以及同一進程不同線程之間的同步手段。
  6. 套接口(Socket):更為一般的進程間通信機制,可用於不同機器之間的進程間通信。起初是由Unix系統的BSD分支開發出來的,但現在一般可以移植到其它類Unix系統上:Linux和System V的變種都支持套接字。

或許你會有疑問,那多線程間要通信,應該怎么做?前面已經說了,多數的多線程都是在同一個進程下的,它們共享該進程的全局變量,我們可以通過全局變量來實現線程間通信。如果是不同的進程下的2個線程間通信,直接參考進程間通信。

 

4.關於線程的堆棧

說一下線程自己的堆棧問題。

是的,生成子線程后,它會獲取一部分該進程的堆棧空間,作為其名義上的獨立的私有空間。(為何是名義上的呢?)由於,這些線程屬於同一個進程,其他線程只要獲取了你私有堆棧上某些數據的指針,其他線程便可以自由訪問你的名義上的私有空間上的數據變量。(注:而多進程是不可以的,因為不同的進程,相同的虛擬地址,基本不可能映射到相同的物理地址)

 

5.在子線程里fork

 

看過好幾次有人問,在子線程函數里調用system或者 fork為何出錯,或者fork產生的子進程是完全復制父進程的嗎?

我測試過,只要你的線程函數滿足前面的要求,都是正常的。

 

[cpp]  view plain copy
 
  1. #include<stdio.h>  
  2. #include<string.h>  
  3. #include<stdlib.h>  
  4. #include<unistd.h>  
  5. #include<pthread.h>  
  6.                                                                                                   
  7. void* task1(void *arg1)  
  8. {  
  9.     printf("task1/n");  
  10.     system("ls");  
  11.     pthread_exit( (void *)1);  
  12. }  
  13.                                                                                                   
  14. int main()  
  15. {  
  16.   int ret=0;  
  17.   void *p;  
  18.    int p1=0;  
  19.    pthread_t pid1;  
  20.     pthread_create(&pid1, NULL, task1, NULL);  
  21.     ret=pthread_join(pid1, &p);  
  22.      printf("end main/n");  
  23.     return 1;  
  24. }  

上面這段代碼就可以正常得調用ls指令。

不過,在同時調用多進程(子進程里也調用線程函數)和多線程的情況下,函數體內很有可能死鎖。

具體的例子可以看看這篇文章。

http://www.cppblog.com/lymons/archive/2008/06/01/51836.aspx

 

 

 

Linux內核對多進程和多線程的支持方式:

        線程機制支持並發程序設計技術,在多處理器上能真正保證並行處理。而在linux實現線程很特別,linux把所有的線程都當作進程實現。linux下線程看起來就像普通進程(只是該進程和其他進程共享資源,如地址空間)。上述機制與Microsoft windows或是Sun Solaris實現差異很大。

        Linux的線程實現是在核外進行的,核內提供的是創建進程的接口do_fork()。內核提供了兩個系統調用__clone()和fork(),最終都用不同的參數調用do_fork()核內API。 do_fork() 提供了很多參數,包括CLONE_VM(共享內存空間)、CLONE_FS(共享文件系統信息)、CLONE_FILES(共享文件描述符表)、CLONE_SIGHAND(共享信號句柄表)和CLONE_PID(共享進程ID,僅對核內進程,即0號進程有效)。當使用fork系統調用產生多進程時,內核調用do_fork()不使用任何共享屬性,進程擁有獨立的運行環境。當使用pthread_create()來創建線程時,則最終設置了所有這些屬性來調用__clone(),而這些參數又全部傳給核內的do_fork(),從而創建的”進程”擁有共享的運行環境,只有棧是獨立的,由 __clone()傳入。

         即:Linux下不管是多線程編程還是多進程編程,最終都是用do_fork實現的多進程編程,只是進程創建時的參數不同,從而導致有不同的共享環境。Linux線程在核內是以輕量級進程的形式存在的,擁有獨立的進程表項,而所有的創建、同步、刪除等操作都在核外pthread庫中進行。pthread 庫使用一個管理線程(__pthread_manager() ,每個進程獨立且唯一)來管理線程的創建和終止,為線程分配線程ID,發送線程相關的信號,而主線程pthread_create()) 的調用者則通過管道將請求信息傳給管理線程。

很多朋友都說使用多線程的好處是資源占用少,其隱含之意就是說進程占用資源比線程多,對吧?但實際上Linux下多進程是否就真的點用很多資源呢?暫且不說進程是否比線程占用資源多,就進程占用資源的多少情況而言,Linux確實是做得相當節省的。產生一個多進程時肯定是要產生的一點內存是要復制進程表項,即一個task_struct結構,但這個結構本身做得相當小巧。其它對於一個進程來說必須有的數據段、代碼段、堆棧段是不是全盤復制呢?對於多進程來說,代碼段是肯定不用復制的,因為父進程和各子進程的代碼段是相同的,數據段和堆棧段呢?也不一定,因為在Linux里廣泛使用的一個技術叫copy-on-write,即寫時拷貝。copy-on-write意味着什么呢?意味着資源節省,假設有一個變量x在父進程里存在,當這個父進程創建一個子進程或多個子進程時這個變量x是否復制到了子進程的內存空間呢?不會的,子進程和父進程使用同一個內存空間的變量,但當子進程或父進程要改變變量x的值時就會復制該變量,從而導致父子進程里的變量值不同。父子進程變量是互不影響的,由於父子進程地址空間是完全隔開的,變量的地址可以是完全相同的

          Linux的”線程”和”進程”實際上處於一個調度層次,共享一個進程標識符空間,這種限制使得不可能在Linux上實現完全意義上的POSIX線程機制,因此眾多的Linux線程庫實現嘗試都只能盡可能實現POSIX的絕大部分語義,並在功能上盡可能逼近。Linux進程的創建是非常迅速的。內核設計與實現一書中甚至指出Linux創建進程的速度和其他針對線程優化的操作系統(Windows,Solaris)創建線程的速度相比,測試結果非常的好,也就是說創建速度很快。由於異步信號是內核以進程為單位分發的,而LinuxThreads的每個線程對內核來說都是一個進程,且沒有實現”線程組”,因此,某些語義不符合POSIX標准,比如沒有實現向進程中所有線程發送信號,README對此作了說明。LinuxThreads中的線程同步很大程度上是建立在信號基礎上的,這種通過內核復雜的信號處理機制的同步方式,效率一直是個問題。LinuxThreads 的問題,特別是兼容性上的問題,嚴重阻礙了Linux上的跨平台應用(如Apache)采用多線程設計,從而使得Linux上的線程應用一直保持在比較低的水平。在Linux社區中,已經有很多人在為改進線程性能而努力,其中既包括用戶級線程庫,也包括核心級和用戶級配合改進的線程庫。目前最為人看好的有兩個項目,一個是RedHat公司牽頭研發的NPTL(Native Posix Thread Library),另一個則是IBM投資開發的NGPT(Next Generation Posix Threading),二者都是圍繞完全兼容POSIX 1003.1c,同時在核內和核外做工作以而實現多對多線程模型。這兩種模型都在一定程度上彌補了LinuxThreads的缺點,且都是重起爐灶全新設計的。

          綜上所述的結論是在Linux下編程多用多進程編程少用多線程編程

         IBM有個家伙做了個測試,發現切換線程context的時候,windows比linux快一倍多。進出最快的鎖(windows2k的 critical section和linux的pthread_mutex),windows比linux的要快五倍左右。當然這並不是說linux不好,而且在經過實際編程之后,綜合來看我覺得linux更適合做high performance server,不過在多線程這個具體的領域內,linux還是稍遜windows一點。這應該是情有可原的,畢竟unix家族都是從多進程過來的,而 windows從頭就是多線程的。

如果是UNIX/linux環境,采用多線程沒必要。

多線程比多進程性能高?誤導!

應該說,多線程比多進程成本低,但性能更低

在UNIX環境,多進程調度開銷比多線程調度開銷,沒有顯著區別,就是說,UNIX進程調度效率是很高的。內存消耗方面,二者只差全局數據區,現在內存都很便宜,服務器內存動輒若干G,根本不是問題。

多進程是立體交通系統,雖然造價高,上坡下坡多耗點油,但是不堵車。

多線程是平面交通系統,造價低,但紅綠燈太多,老堵車。

我們現在都開跑車,油(主頻)有的是,不怕上坡下坡,就怕堵車。

高性能交易服務器中間件,如TUXEDO,都是主張多進程的。實際測試表明,TUXEDO性能和並發效率是非常高的。TUXEDO是貝爾實驗室的,與UNIX同宗,應該是對UNIX理解最為深刻的,他們的意見應該具有很大的參考意義。

 

多進程多線程優缺點:

在Linux下編程多用多進程編程少用多線程編程

         IBM有個家伙做了個測試,發現切換線程context的時候,windows比linux快一倍多。進出最快的鎖(windows2k的 critical section和linux的pthread_mutex),windows比linux的要快五倍左右。當然這並不是說linux不好,而且在經過實際編程之后,綜合來看我覺得linux更適合做high performance server,不過在多線程這個具體的領域內,linux還是稍遜windows一點。這應該是情有可原的,畢竟unix家族都是從多進程過來的,而 windows從頭就是多線程的。

如果是UNIX/linux環境,采用多線程沒必要。

多線程比多進程性能高?誤導!

應該說,多線程比多進程成本低,但性能更低

在UNIX環境,多進程調度開銷比多線程調度開銷,沒有顯著區別,就是說,UNIX進程調度效率是很高的。內存消耗方面,二者只差全局數據區,現在內存都很便宜,服務器內存動輒若干G,根本不是問題。

多進程是立體交通系統,雖然造價高,上坡下坡多耗點油,但是不堵車。

多線程是平面交通系統,造價低,但紅綠燈太多,老堵車。

我們現在都開跑車,油(主頻)有的是,不怕上坡下坡,就怕堵車。

高性能交易服務器中間件,如TUXEDO,都是主張多進程的。實際測試表明,TUXEDO性能和並發效率是非常高的。TUXEDO是貝爾實驗室的,與UNIX同宗,應該是對UNIX理解最為深刻的,他們的意見應該具有很大的參考意義。

From:

http://blogold.chinaunix.net/u3/115895/showart_2428975.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM