標籤:style blog http io ar os 使用 sp for
本文轉載至 http://daimajishu.iteye.com/blog/1557076
很想寫點關於多進程和多線程的東西,我確實很愛他們。但是每每想動手寫點關於他們的東西,卻總是求全心理作祟,始終動不了手。
今天終於下了決心,寫點東西,以後可以再修修補補也無妨。
一.為何需要多進程(或者多線程),為何需要並發?
這個問題或許本身都不是個問題。但是對於沒有接觸過多進程編程的朋友來說,他們確實無法感受到並發的魅力以及必要性。
我想,只要你不是整天都寫那種int main()到底的代碼的人,那麼或多或少你會遇到代碼響應不夠用的情況,也應該有嘗過並發編程的甜頭。就像一個快餐點的服務員,既要在前台接待客戶點餐,又要接電話送外賣,沒有分身術肯定會忙得你焦頭爛額的。幸運的是確實有這麼一種技術,讓你可以像孫悟空一樣分身,靈魂出竅,樂哉樂哉地輕鬆應付一切狀況,這就是多進程/線程技術。
並發技術,就是可以讓你在同一時間同時執行多條任務的技術。你的代碼將不僅僅是從上到下,從左至右這樣規規矩矩的一條線執行。你可以一條線在main函數裡跟你的客戶交流,另一條線,你早就把你外賣送到了其他客戶的手裡。
所以,為何需要並發?因為我們需要更強大的功能,提供更多的服務,所以並發,必不可少。
二.多進程
什麼是進程。最直觀的就是一個個pid,官方的說法就:進程是程式在電腦上的一次執行活動。
說得簡單點,下面這段代碼執行的時候
view plain
- int main()
- {
- printf(”pid is %d/n”,getpid() );
- return 0;
- }
view plain
- int main()
- {
- printf(”pid is %d/n”,getpid() );
- return 0;
- }
進入main函數,這就是一個進程,進程pid會列印出來,然後運行到return,該函數就退出,然後由於該函數是該進程的唯一的一次執行,所以return後,該進程也會退出。
看看多進程。linux下建立子進程的調用是fork();
view plain
- #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();
- }
- }
view plain
- #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 |
第一個參數為指向線程標識符的指標。
第二個參數用來設定線程屬性。
第三個參數是線程運行函數的起始地址。
最後一個參數是運行函數的參數。
view plain
- #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);
- }
view plain
- #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下處理序間通訊的幾種主要手段簡介:
- 管道(Pipe)及有名管道(named pipe):管道可用於具有親緣關係進程間的通訊,有名管道克服了管道沒有名字的限制,因此,除具有管道所具有的功能外,它還允許無親緣關係進程間的通訊;
- 訊號(Signal):訊號是比較複雜的通訊方式,用於通知接受進程有某種事件發生,除了用於處理序間通訊外,進程還可以發送訊號給進程本身;linux除了支援Unix早期訊號語義函數sigal外,還支援語義符合Posix.1標準的訊號函數sigaction(實際上,該函數是基於BSD的,BSD為了實現可靠訊號機制,又能夠統一對外介面,用sigaction函數重新實現了signal函數);
- 報文(Message)隊列(訊息佇列):訊息佇列是訊息的連結資料表,包括Posix訊息佇列system V訊息佇列。有足夠許可權的進程可以向隊列中添加訊息,被賦予讀許可權的進程則可以讀走隊列中的訊息。訊息佇列克服了訊號承載資訊量少,管道只能承載無格式位元組流以及緩衝區大小受限等缺點。
- 共用記憶體:使得多個進程可以訪問同一塊記憶體空間,是最快的可用IPC形式。是針對其他通訊機制運行效率較低而設計的。往往與其它通訊機制,如訊號量結合使用,來達到進程間的同步及互斥。
- 訊號量(semaphore):主要作為進程間以及同一進程不同線程之間的同步手段。
- 套介面(Socket):更為一般的處理序間通訊機制,可用於不同機器之間的處理序間通訊。起初是由Unix系統的BSD分支開發出來的,但現在一般可以移植到其它類Unix系統上:Linux和System V的變種都支援通訊端。
或許你會有疑問,那多線程間要通訊,應該怎麼做?前面已經說了,多數的多線程都是在同一個進程下的,它們共用該進程的全域變數,我們可以通過全域變數來實現線程間通訊。如果是不同的進程下的2個線程間通訊,直接參考處理序間通訊。
4.關於線程的堆棧
說一下線程自己的堆棧問題。
是的,產生子線程後,它會擷取一部分該進程的堆棧空間,作為其名義上的獨立的私人空間。(為何是名義上的呢?)由於,這些線程屬於同一個進程,其他線程只要擷取了你私人堆棧上某些資料的指標,其他線程便可以自由訪問你的名義上的私人空間上的資料變數。(註:而多進程是不可以的,因為不同的進程,相同的虛擬位址,基本不可能映射到相同的物理地址)
5.在子線程裡fork
看過好幾次有人問,在子線程函數裡調用system或者 fork為何出錯,或者fork產生的子進程是完全複製父進程的嗎?
我測試過,只要你的線程函數滿足前面的要求,都是正常的。
view plain
- #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;
- }
view plain
- #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
ios多線程和進程的區別(轉載)