多線程和多進程之間的區別(總結)


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

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

 

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

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

我想。僅僅要你不是整天都寫那種int main()究竟的代碼的人,那么或多或少你會遇到代碼響應不夠用的情況,也應該有嘗過並發編程的甜頭。

就像一個快餐點的服務員,既要在前台接待客戶點餐,又要接電話送外賣。沒有分身術肯定會忙得你焦頭爛額的。幸運的是確實有這么一種技術,讓你能夠像孫悟空一樣分身。靈魂出竅,樂哉樂哉地輕松應付一切狀況,這就是多進程/線程技術。

並發技術,就是能夠讓你在同一時間同一時候運行多條任務的技術。你的代碼將不不過從上到下,從左到右這樣規規矩矩的一條線運行。你能夠一條線在main函數里跟你的客戶交流,還有一條線,你早就把你外賣送到了其它客戶的手里。

 

所以,為何須要並發?由於我們須要更強大的功能。提供很多其它的服務,所以並發,不可缺少。

 

.多進程

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

說得簡單點,以下這段代碼運行的時候

int main()

{

printf(”pid is %d/n”,getpid() );

return 0;

}

 

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

 

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

  

#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,一次返回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

第一個參數為指向線程標識符的指針。
第二個參數用來設置線程屬性。


第三個參數是線程執行函數的起始地址。
最后一個參數是執行函數的參數。

   

#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產生的子進程是全然復制父進程的嗎?

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

 

#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:臨時寫這個現在。總結的東西,它似乎真的不適合我寫。免費,想到什么,它修改一下再回來。

 

 


免責聲明!

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



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