多執行緒和多程式的區別(小結)

pamxy發表於2013-07-02

轉自:http://blog.csdn.net/leo115/article/details/8147035

總結的很好,學習下:http://blog.csdn.net/hairetz/article/details/4281931/

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

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

 

.為何需要多程式(或者多執行緒),為何需要併發?

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

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

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

 

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

 

.多程式

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

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

  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();

  

  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

第一個引數為指向執行緒識別符號的指標。
第二個引數用來設定執行緒屬性。
第三個引數是執行緒執行函式的起始地址。
最後一個引數是執行函式的引數。

   

  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產生的子程式是完全複製父程式的嗎?

我測試過,只要你的執行緒函式滿足前面的要求,都是正常的。

 

  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

 

 

 

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


相關文章