標籤:多線程 多進程
關於多進程和多線程,一直想寫點什麼來進行一次總結,今天終於提筆了,若有講解錯誤之處,希望廣大讀者能給予指正。,我想從以下幾個方面進行一次詳解劃分.第一,運用。第二,同步。第三,通訊。第四,選擇。
那麼閑話少說,開始第一個,關於線程和進程之間的運用。
什麼是進程?
有一個很官方的說法:進程是程式在電腦上的一次執行活動。但我覺得,可能這句話有點不對,應該換成進程加線程是程式在電腦上的一次執行活動才更加的合理。因為進程是資源分派的最小單位,線程是CPU調度的最小單位。所以我們看到進程的時候,應該聯想到資源,看到線程的時候聯想到程式的執行,程式真正再跑的只是線程,例如程式開始的時候,就是一個線程利用進程的資源再跑。
650) this.width=650;" align="left" src="http://img.blog.csdn.net/20150513105025850?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd2dqaW4xMTA2/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" />
列印出進程ID和主線程ID。
如何建立進程?
進程的建立,我們最容易想象到的兩個函數是fork和vfork。
fork在建立的子進程複製了父進程的資源,新舊進程使用同一個程式碼片段,複製了資料區段和堆棧段,但是當進程開始啟動並執行時候,新舊的地址空間在實體記憶體上開始劃分開來,這裡使用了copy_on_write技術來達到真正意義上的分開。兩者獨立運行,也就會在資料區段和堆棧段上的資料不會有衝突。fork的優點取決父子進程完全獨立,這樣具有很好的並發性。
vfork在建立子進程的時候子進程是完全運行在父進程的地址空間上,使用vfork建立的子進程,筆者在這強烈建議使用exec函數族,因為使用exec函數族,執行了另一個程式,子進程不會對父進程的地址空間有任何引用,若需求上面不能執行exec函數族,那麼子進程必須使用_exit()函數進行退出,也不能是執行exit()函數,因為exit函數會將標準輸入輸出資料流進行重新整理,釋放所佔有的資源以及清空緩衝區,而_exit()沒有重新整理緩衝區功能,這裡是考慮到vfork父子進程使用同一個記憶體空間,不然程式會出現意料之外的結果,比如如所示程式就出現三個進程,兩個子進程和一個父進程,只有當子進程執行了_exit(),exec函數族等函數的時候,父進程才會正常執行。所以在此建議讀者,如果需要使用exec函數族的時候,就用vfork()建立,不然就用fork()建立子進程.
650) this.width=650;" align="left" src="http://img.blog.csdn.net/20150513114919927?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd2dqaW4xMTA2/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" />
運行結果是
num=0
子進程pid=29356
num=7917675
父進程pid=29355
num=0
子進程pid=29360
進程的等待,父進程可以用 wait和waitpid函數等待子進程的結束,wait是等待 任意一個子進程結束,waitpid 可以對等待的方式進行設定。如果沒有 子進程,直接返回。如果沒有結束的子進程, 父進程將被 阻塞(waitpid可以設定非阻塞)。wait、waitpid都可以取得 子進程的結束狀態。pid_t wait(int *status),返回結束的子進程id,參數用於帶出結束狀態,pid_t waitpid(pid_t pid,int* status,int options),返回結束的子進程id,參數status用於 帶出結束狀態,options可以指定非阻塞,一般用0代表阻塞。結束狀態 status可以用 宏做一些判斷:WIFEXITED(status) 可以判斷子進程是否正常結束。
#include<pthread.h> #include<stdio.h> #include<stdlib.h> #include<signal.h> int main(int argc,char* argv[]) { int status=0; pid_t pid=fork(); if(pid<0) { perror("fork"); exit(1); } else if(pid==0) { sleep(1);//讓進程進行休眠狀態,保證父進程先執行 printf("子進程pid=%d\n",getpid()); exit(5); } else { wait(&status);//程式進行阻塞狀態,看其是否是等待子進程執行完畢 printf("父進程pid=%d,%d\n",getpid(),status); if(WIFEXITED(status)) { printf("the child process exit normally\n"); } else { printf("the child process exit abnormally\n"); } } }<span style="font-family:KaiTi_GB2312;"><span style="font-size:18px;"></span></span>
進程的結束,進程的結束可以分程式執行完畢,自然結束,也可以由int kill(pid_t pid, int sig);向指定的進程號發送訊號,如果發送9訊號,預設是殺掉指定的進程,因為考慮到資源問題,擔心一些動態記憶體在進程裡面申請後,正準備釋放之前該進程被其它進程kill了,導致出現記憶體泄露,所以我們盡量避免使用kill這樣暴力的函數
父子進程之間的執行,注線上程裡面沒有父子線程關係,只有主線程和其它線程.父進程啟動子進程後,父子進程 同時運行。如果子進程先結束,子進程 給父進程發訊號,父進程 負責回收子進程的資源。 父進程啟動子進程後,父子進程 同時運行。如果父進程先結束,子進程成 孤兒進程,子進程 認 init進程(進程1)做新的父進程,init進程 也叫 孤兒院。父進程啟動子進程後,父子進程 同時運行。如果子進程先結束,子進程 給父進程發訊號,但父進程有可能沒收到訊號,或者收到了沒處理,子進程 會變成 殭屍進程
線程的建立,使用函數int pthread_create(pthread_t *tidp,const pthread_attr_t *attr,(void*)(*start_rtn)(void*),void *arg);第一個參數是指向線程標示符的指標,線程標示符,一個無符號的長整型的類型,在標頭檔裡面我們可以看到它的定義typedef unsigned long int pthread_t;第二個參數是指向線程屬性的指標,這個如若不設定屬性,那麼設定為空白,一切就以預設屬性,筆者覺得這個屬性比較重要,將在後面詳細講解.第三個參數,顧名思義是一個函數指標,函數的傳回值類型是無符號的指標類型,第四個參數是函數指標函數的傳遞參數,如若沒有參數,則可以設定空,如果需要傳遞多個參數,可定義結構體進行傳遞,不過傳遞的時候要強制轉換成void*類型。
線程的等待,使用函數pthread_join(),該函數是當線程結束的時候,清收線程的資源,然後函數返回,這裡可以保證不會存在主線程結束了,開啟的線程還麼開始執行,或者只是執行了一半,有讀者可能會有疑問,不是說線程是執行單位,進程才是資源單位,為什麼還有清理資源工作,這裡我們需要瞭解到其實在Linux中,建立的線程並不是在原先的進程中,而是系統通過一個系統調用函數clone()。該系統調用copy了一個和原先進程完全一樣的進程,並在這個進程中執行線程函數。不過這個copy過程和fork不一樣。 copy後的進程和原先的進程共用了所有的變數(有點像是vfork的功能),運行環境。這樣既可以做到多線程之間共用全域變數。所以pthread_join函數保證了線程的全部執行和資源清理工作。
線程的屬性,也就是在建立線程函數pthread_create裡面的第三個參數pthread_attr_t *attr,首先我們要知道,pthread_attr_t是一個結構體類型,如下所示
typedef struct
{
int detachstate; 線程的分離狀態
int schedpolicy; 線程調度策略
struct sched_param schedparam; 線程的調度參數
int inheritsched; 線程的繼承性
int scope; 線程的範圍
size_t guardsize; 線程棧末尾的警戒緩衝區大小
int stackaddr_set;
void * stackaddr; 線程棧的位置
size_t stacksize; 線程棧的大小
}pthread_attr_t;
屬性pthread_attr_t主要包括scope屬性、detach屬性、堆棧地址、堆棧大小、優先順序。在這裡就注重講解堆棧大小屬性的設定及其應用,至於其它屬性值,讀者若是有興趣,可以參考http://blog.csdn.net/zsf8701/article/details/7843837,這篇部落格.在很多的時候,我們需要增加並發數量來完成某一項認為,特別是在網路編程裡面服務端程式上支援多並發處理,我們需要更多的並發量去支援,我們假設使用的電腦CPU是32位元,最大的定址範圍是4GB,1GB是所有的進程共用的核心空間,3GB是使用者空間,也就是進程虛擬記憶體空間,我們在linux下面用ulimit -s 可以產看當前系統上線程的堆棧空間大小是多少,一般預設是8M,這樣我們可以計算出線程的最大上限數量是: (1024*1024*1024*3) / (1024*1024*8) = 384,實際數字應該略小384,因為還要計算程式文本、資料、共用庫等佔用的空間。當前的WEB伺服器上面並發量大於384已經是很平常的事了,那麼我們如何突破這個限制,已達到更多的並發量呢,有兩種辦法,第一是通過ulimit -s 去修改堆棧的空間大小,比如 ulimit -s 1024 每個線程只分配1M的空間,那麼並發量就可以增加到384*8,可以根據自己的項目需求去更改這個限制,但是這樣做的缺點是修改了,那麼所有在本機器上面跑的進程,都會預設這個設定,就會讓一些進程因為堆棧空間太小導致段錯誤,所以顯然這個不是一個很理智的辦法。於是我們就有了線程設定堆棧大小的屬性,保證本次受影響的線程只是設定這個屬性的線程。說了這麼多,我們就開始用實際代碼練練手。
#include<stdio.h>#include<string.h> #include<limits.h>#include<pthread.h>void* fun(void* i);int main(int argc,char* argv[]) { pthread_t t[1000]; pthread_attr_t attr; int i=0; if(pthread_attr_init(&attr))//初始化一個屬性 { perror("attr"); } if(pthread_attr_setstacksize(&attr,163840))//設定線程的堆棧大小,我們開始利用初始化的屬性進行堆棧空間大小的設定,這裡是以位元組為單位的堆棧空間大小的調整 { perror("attr\n"); } for(i=0;i<1000;i++) { pthread_create(&t[i],&attr,fun,NULL);//開啟一個線程 } for(i=0;i<1000;i++) { pthread_join(t[i],NULL); } printf("%d\n",PTHREAD_STACK_MIN);//這裡需要牽扯到一個知識點,我們在設定堆棧空間大小的時候,不能小於linux內部定義的最小堆棧大小,可以列印這個宏看看是多少,根據作業系統不同而不同,我這裡是16384 } void* fun(void*i) { }
在設定屬性值的時候,注意點是第一先利用pthread_attr_init初始化一個屬性,後續才能利用這個初始化好的屬性值。
在設定堆棧大小上,不得不提到一個重要的屬性是線程棧末尾的警戒緩衝區,int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);設定線程的堆棧保護區,第二個參數設定堆棧的保護區,一般預設是4096個位元組,一旦線程棧在使用中溢出併到達了這片記憶體,程式可以捕獲系統核心發出的警示訊號,然後使用 malloc擷取另外的記憶體,並通過stackaddr改變線程棧的位置,以獲得額外的棧空間,這個動態擴充棧空間辦法需要手工編程。
線程的結束,我們在啟動一個線程,強烈建議不要在外部用pthread_kill,pthread_cancel等函數強行的中端一個線程,這樣容易導致很多問題,因為這樣的強行取消我們不清楚線程終止的地方會是在哪裡,可能會在這之前動態申請了記憶體,而沒有得到釋放,更嚴重的是在剛進入一個加鎖代碼中,然後被強制退出,那麼就會導致死結現象,所以建議讓線程能夠自己執行完,讓所有的資源能夠得到釋放,若必須使用外部幹擾,那麼我們需要線上程裡面設定安全的取消點,也就是當線程接收到cancel的時候,不會馬上退出,運行到一個相對比較安全的地方,然後再退出本次線程,設定取消點上,我們需要讓本次線程能夠做到延遲取消 pthread_setcancelstate(PTHREAD_CANCEL_DISABLE,NULL);線上程設定這個屬性,然後設定一個取消點,取消點有很多的線程函數都可以,常用的是pthread_testcancel(),pthread_join()等
第二個,同步
linux下的多進程,多線程編程