多線程和多進程的區別(小結)

分類: linux 2009-06-19 09:33 14421人閱讀 評論(16) 收藏 舉報

很想寫點關於多進程和多線程的東西,我確實很愛他們。可是往往想動手寫點關於他們的東西,卻老是求全心理做祟,始終動不了手。linux

今天終於下了決心,寫點東西,之後能夠再修修補補也無妨。編程

 

.爲什麼須要多進程(或者多線程),爲什麼須要併發?安全

這個問題或許自己都不是個問題。可是對於沒有接觸過多進程編程的朋友來講,他們確實沒法感覺到併發的魅力以及必要性。多線程

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

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

 

因此,爲什麼須要併發?由於咱們須要更強大的功能,提供更多的服務,因此併發,必不可少。測試

 

.多進程google

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

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

  1. int main()  
  2.   
  3. {  
  4.   
  5. printf(」pid is %d/n」,getpid() );  
  6.   
  7. return 0;  
  8.   
  9. }  
int main() { printf(」pid is %d/n」,getpid() ); return 0; }

 

進入main函數,這就是一個進程,進程pid會打印出來,而後運行到return,該函數就退出,而後因爲該函數是該進程的惟一的一次執行,因此return後,該進程也會退出。

 

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

  

  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. }  
#include <unistd.h> #include <sys/types.h> #include <stdio.h> void print_exit() { printf("the exit pid:%d/n",getpid() ); } main () { pid_t pid; atexit( print_exit ); //註冊該進程退出時的回調函數 pid=fork(); if (pid < 0) printf("error in fork!"); else if (pid == 0) printf("i am the child process, my process id is %d/n",getpid()); else { printf("i am the parent process, my process id is %d/n",getpid()); sleep(2); wait(); } }


 

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,一次返回,順序執行下面的代碼。這是子進程。

一次返回子進程的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

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

   

  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. }  
#include<stdio.h> #include<string.h> #include<stdlib.h> #include<unistd.h> #include<pthread.h> void* task1(void*); void* task2(void*); void usr(); int p1,p2; int main() { usr(); getchar(); return 1; } void usr() { pthread_t pid1, pid2; pthread_attr_t attr; void *p; int ret=0; pthread_attr_init(&attr); //初始化線程屬性結構 pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); //設置attr結構爲分離 pthread_create(&pid1, &attr, task1, NULL); //建立線程,返回線程號給pid1,線程屬性設置爲attr的屬性,線程函數入口爲task1,參數爲NULL pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); pthread_create(&pid2, &attr, task2, NULL); //前臺工做 ret=pthread_join(pid2, &p); //等待pid2返回,返回值賦給p printf("after pthread2:ret=%d,p=%d/n", ret,(int)p); } void* task1(void *arg1) { printf("task1/n"); //艱苦而沒法預料的工做,設置爲分離線程,任其自生自滅 pthread_exit( (void *)1); } void* task2(void *arg2) { int i=0; printf("thread2 begin./n"); //繼續送外賣的工做 pthread_exit((void *)2); }


 

這個多線程的例子應該很明瞭了,主線程作本身的事情,生成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產生的子進程是徹底複製父進程的嗎?

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

 

  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. }  
#include<stdio.h> #include<string.h> #include<stdlib.h> #include<unistd.h> #include<pthread.h> void* task1(void *arg1) { printf("task1/n"); system("ls"); pthread_exit( (void *)1); } int main() { int ret=0; void *p; int p1=0; pthread_t pid1; pthread_create(&pid1, NULL, task1, NULL); ret=pthread_join(pid1, &p); printf("end main/n"); return 1; }


 

 

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

 

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

具體的例子能夠看看這篇文章。

 

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

 

 

 

End:暫時寫到這吧,總結這東西,看來真不適合我寫。有空了,想到什麼了,再回來修修補補吧。

相關文章
相關標籤/搜索