文章目錄
5 線程編程應(yīng)用開發(fā)
5.1 線程的使用
5.1.1 為什么要使用多線程
5.1.2 線程概念
5.1.3 線程的標(biāo)識pthread_t
5.1.4 線程的創(chuàng)建
5.1.5 向線程傳入參數(shù)
5.1.6 線程的退出與回收
5.2 線程的控制
5.2.1 多線程編臨界資源訪問
5.2.2 互斥鎖API簡述
5.2.3 多線程編執(zhí)行順序控制
5.2.4 信號量API簡述
5.3 總結(jié)
5 線程編程應(yīng)用開發(fā)
? 本章將分為兩大部分進(jìn)行講解,前半部分將引出線程的使用場景及基本概念,通過示例代碼來說明一個線程創(chuàng)建到退出到回收的基本流程。后半部分則會通過示例代碼來說明如果控制好線程,從臨界資源訪問與線程的執(zhí)行順序控制上引出互斥鎖、信號量的概念與使用方法。
5.1 線程的使用
5.1.1 為什么要使用多線程
? 在編寫代碼時,是否會遇到以下的場景會感覺到難以下手?
場景一:寫程序在拷貝文件時,需要一邊去拷貝文件,一邊去向用戶展示拷貝文件的進(jìn)度時,傳統(tǒng)做法是通過每次拷貝完成結(jié)束后去更新變量,再將變量轉(zhuǎn)化為進(jìn)度顯示出來。其中經(jīng)歷了拷貝->計算->顯示->拷貝->計算->顯示…直至拷貝結(jié)束。這樣的程序架構(gòu)及其的低效,必須在單次拷貝結(jié)束后才可以刷新當(dāng)前拷貝進(jìn)度,若可以將進(jìn)程分支,一支單獨(dú)的解決拷貝問題,一支單獨(dú)的解決計算刷新問題,則程序效率會提升很多。
場景二:用阻塞方式去讀取數(shù)據(jù),實時需要發(fā)送數(shù)據(jù)的時候。例如在進(jìn)行串口數(shù)據(jù)傳輸或者網(wǎng)絡(luò)數(shù)據(jù)傳輸?shù)臅r候,我們往往需要雙向通信,當(dāng)設(shè)置讀取數(shù)據(jù)為阻塞模式時候,傳統(tǒng)的單線程只能等到數(shù)據(jù)接收來臨后才能沖過阻塞,再根據(jù)邏輯進(jìn)行發(fā)送。當(dāng)我們要實現(xiàn)隨時發(fā)送、隨時接收時,無法滿足我們的業(yè)務(wù)需求。若可以將進(jìn)程分支,一支單純的處理接收數(shù)據(jù)邏輯,一支單純的解決發(fā)送數(shù)據(jù)邏輯,就可以完美的實現(xiàn)功能。
? 基于以上場景描述,多線程編程可以完美的解決上述問題。
5.1.2 線程概念
? 所謂線程,就是操作系統(tǒng)所能調(diào)度的最小單位。普通的進(jìn)程,只有一個線程在執(zhí)行對應(yīng)的邏輯。我們可以通過多線程編程,使一個進(jìn)程可以去執(zhí)行多個不同的任務(wù)。相比多進(jìn)程編程而言,線程享有共享資源,即在進(jìn)程中出現(xiàn)的全局變量,每個線程都可以去訪問它,與進(jìn)程共享“4G”內(nèi)存空間,使得系統(tǒng)資源消耗減少。本章節(jié)來討論Linux下POSIX線程。
5.1.3 線程的標(biāo)識pthread_t
? 對于進(jìn)程而言,每一個進(jìn)程都有一個唯一對應(yīng)的PID號來表示該進(jìn)程,而對于線程而言,也有一個“類似于進(jìn)程的PID號”,名為tid,其本質(zhì)是一個pthread_t類型的變量。線程號與進(jìn)程號是表示線程和進(jìn)程的唯一標(biāo)識,但是對于線程號而言,其僅僅在其所屬的進(jìn)程上下文中才有意義。
獲取線程號 #include pthread_t pthread_self(void); 成功:返回線程號
在程序中,可以通過函數(shù)pthread_self,來返回當(dāng)前線程的線程號,例程1給出了打印線程tid號。
測試?yán)?:(Phtread_txex1.c)
1 #include 2 #include 3 4 int main() 5 { 6 pthread_t tid = pthread_self();//獲取主線程的tid號 7 printf("tid = %lun",(unsigned long)tid); 8 return 0; 9 }
注意:因采用POSIX線程接口,故在要編譯的時候包含pthread庫,使用gcc編譯應(yīng)gcc xxx.c -lpthread 方可編譯多線程程序。
編譯結(jié)果:
5.1.4 線程的創(chuàng)建
創(chuàng)建線程 #include int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg); 成功:返回0
? 在傳統(tǒng)的程序中,一個進(jìn)程只有一個線程,可以通過函數(shù)pthread_create來創(chuàng)建線程。
? 該函數(shù)第一個參數(shù)為pthread_t類型的線程號地址,當(dāng)函數(shù)執(zhí)行成功后會指向新建線程的線程號;第二個參數(shù)表示了線程的屬性,一般傳入NULL表示默認(rèn)屬性;第三個參數(shù)代表返回值為void *,形參為void *的函數(shù)指針,當(dāng)線程創(chuàng)建成功后,會自動的執(zhí)行該回調(diào)函數(shù);第四個參數(shù)則表示為向線程處理函數(shù)傳入的參數(shù),若不傳入,可用NULL填充,有關(guān)線程傳參后續(xù)小節(jié)會有詳細(xì)的說明,接下來通過一個簡單例程來使用該函數(shù)創(chuàng)建出一個線程。
測試?yán)?:(Phtread_txex2.c)
1 #include 2 #include 3 #include 4 #include 5 6 void *fun(void *arg) 7 { 8 printf("pthread_New = %lun",(unsigned long)pthread_self());//打印線程的tid號 9 } 10 11 int main() 12 { 13 14 pthread_t tid1; 15 int ret = pthread_create(&tid1,NULL,fun,NULL);//創(chuàng)建線程 16 if(ret != 0){ 17 perror("pthread_create"); 18 return -1; 19 } 20 21 /*tid_main 為通過pthread_self獲取的線程ID,tid_new通過執(zhí)行pthread_create成功后tid指向的空間*/ 22 printf("tid_main = %lu tid_new = %lu n",(unsigned long)pthread_self(),(unsigned long)tid1); 23 24 /*因線程執(zhí)行順序隨機(jī),不加sleep可能導(dǎo)致主線程先執(zhí)行,導(dǎo)致進(jìn)程結(jié)束,無法執(zhí)行到子線程*/ 25 sleep(1); 26 27 return 0; 28 } 29
運(yùn)行結(jié)果:
? 通過pthread_create確實可以創(chuàng)建出來線程,主線程中執(zhí)行pthread_create后的tid指向了線程號空間,與子線程通過函數(shù)pthread_self打印出來的線程號一致。
? 特別說明的是,當(dāng)主線程伴隨進(jìn)程結(jié)束時,所創(chuàng)建出來的線程也會立即結(jié)束,不會繼續(xù)執(zhí)行。并且創(chuàng)建出來的線程的執(zhí)行順序是隨機(jī)競爭的,并不能保證哪一個線程會先運(yùn)行??梢詫⑸鲜龃a中sleep函數(shù)進(jìn)行注釋,觀察實驗現(xiàn)象。
去掉上述代碼25行后運(yùn)行結(jié)果:
? 上述運(yùn)行代碼3次,其中有2次被進(jìn)程結(jié)束,無法執(zhí)行到子線程的邏輯,最后一次則執(zhí)行到了子線程邏輯后結(jié)束的進(jìn)程。如此可以說明,線程的執(zhí)行順序不受控制,且整個進(jìn)程結(jié)束后所產(chǎn)生的線程也隨之被釋放,在后續(xù)內(nèi)容中將會描述如何控制線程執(zhí)行。
5.1.5 向線程傳入?yún)?shù)
? pthread_create()的最后一個參數(shù)的為void *類型的數(shù)據(jù),表示可以向線程傳遞一個void *數(shù)據(jù)類型的參數(shù),線程的回調(diào)函數(shù)中可以獲取該參數(shù),例程3舉例了如何向線程傳入變量地址與變量值。
測試?yán)?:(Phtread_txex3.c)
1 #include 2 #include 3 #include 4 #include 5 6 void *fun1(void *arg) 7 { 8 printf("%s:arg = %d Addr = %pn",__FUNCTION__,*(int *)arg,arg); 9 } 10 11 void *fun2(void *arg) 12 { 13 printf("%s:arg = %d Addr = %pn",__FUNCTION__,(int)(long)arg,arg); 14 } 15 16 int main() 17 { 18 19 pthread_t tid1,tid2; 20 int a = 50; 21 int ret = pthread_create(&tid1,NULL,fun1,(void *)&a);//創(chuàng)建線程傳入變量a的地址 22 if(ret != 0){ 23 perror("pthread_create"); 24 return -1; 25 } 27 ret = pthread_create(&tid2,NULL,fun2,(void *)(long)a);//創(chuàng)建線程傳入變量a的值 28 if(ret != 0){ 29 perror("pthread_create"); 30 return -1; 31 } 32 sleep(1); 33 printf("%s:a = %d Add = %p n",__FUNCTION__,a,&a); 34 return 0; 35 } 36
運(yùn)行結(jié)果:
? 本例程展示了如何利用線程創(chuàng)建函數(shù)的第四個參數(shù)向線程傳入數(shù)據(jù),舉例了如何以地址的方式傳入值、以變量的方式傳入值,例程代碼的21行,是將變量a先行取地址后,再次強(qiáng)制類型轉(zhuǎn)化為void后傳入線程,線程處理的回調(diào)函數(shù)中,先將萬能指針void *轉(zhuǎn)化為int *,再次取地址就可以獲得該地址變量的值,其本質(zhì)在于地址的傳遞。例程代碼的27行,直接將int類型的變量強(qiáng)制轉(zhuǎn)化為void *進(jìn)行傳遞(針對不同位數(shù)機(jī)器,指針對其字?jǐn)?shù)不同,需要int轉(zhuǎn)化為long在轉(zhuǎn)指針,否則可能會發(fā)生警告),在線程處理回調(diào)函數(shù)中,直接將void *數(shù)據(jù)轉(zhuǎn)化為int類型即可,本質(zhì)上是在傳遞變量a的值。
? 上述兩種方法均可得到所要的值,但是要注意其本質(zhì),一個為地址傳遞,一個為值的傳遞。當(dāng)變量發(fā)生改變時候,傳遞地址后,該地址所對應(yīng)的變量也會發(fā)生改變,但傳入變量值的時候,即使地址指針?biāo)傅淖兞堪l(fā)生變化,但傳入的為變量值,不會受到指針的指向的影響,實際項目中切記兩者之間的區(qū)別。具體說明見例程4.
測試?yán)?:(Phtread_txex4.c)
1 #include 2 #include 3 #include 4 #include 5 6 void *fun1(void *arg) 7 { 8 while(1){ 9 10 printf("%s:arg = %d Addr = %pn",__FUNCTION__,*(int *)arg,arg); 11 sleep(1); 12 } 13 } 14 15 void *fun2(void *arg) 16 { 17 while(1){ 18 19 printf("%s:arg = %d Addr = %pn",__FUNCTION__,(int)(long)arg,arg); 20 sleep(1); 21 } 22 } 23 24 int main() 25 { 26 27 pthread_t tid1,tid2; 28 int a = 50; 29 int ret = pthread_create(&tid1,NULL,fun1,(void *)&a); 30 if(ret != 0){ 31 perror("pthread_create"); 32 return -1; 33 } 34 sleep(1); 35 ret = pthread_create(&tid2,NULL,fun2,(void *)(long)a); 36 if(ret != 0){ 37 perror("pthread_create"); 38 return -1; 39 } 40 while(1){ 41 a++; 42 sleep(1); 43 printf("%s:a = %d Add = %p n",__FUNCTION__,a,&a); 44 } 45 return 0; 46 } 47
運(yùn)行結(jié)果:
? 上述例程講述了如何向線程傳遞一個參數(shù),在處理實際項目中,往往會遇到傳遞多個參數(shù)的問題,我們可以通過結(jié)構(gòu)體來進(jìn)行傳遞,解決此問題。
測試?yán)?:(Phtread_txex5.c)
1 #include 2 #include 3 #include 4 #include 5 #include 6 7 struct Stu{ 8 int Id; 9 char Name[32]; 10 float Mark; 11 }; 12 13 void *fun1(void *arg) 14 { 15 struct Stu *tmp = (struct Stu *)arg; 16 printf("%s:Id = %d Name = %s Mark = %.2fn",__FUNCTION__,tmp->Id,tmp->Name,tmp->Mark); 17 18 } 19 20 int main() 21 { 22 23 pthread_t tid1,tid2; 24 struct Stu stu; 25 stu.Id = 10000; 26 strcpy(stu.Name,"ZhangSan"); 27 stu.Mark = 94.6; 28 29 int ret = pthread_create(&tid1,NULL,fun1,(void *)&stu); 30 if(ret != 0){ 31 perror("pthread_create"); 32 return -1; 33 } 34 printf("%s:Id = %d Name = %s Mark = %.2fn",__FUNCTION__,stu.Id,stu.Name,stu.Mark); 35 sleep(1); 36 return 0; 37 } 38
運(yùn)行結(jié)果:
5.1.6 線程的退出與回收
? 線程的退出情況有三種:第一種是進(jìn)程結(jié)束,進(jìn)程中所有的線程也會隨之結(jié)束。第二種是通過函數(shù)pthread_exit來主動的退出線程。第三種通過函數(shù)pthread_cancel被其他線程被動結(jié)束。當(dāng)線程結(jié)束后,主線程可以通過函數(shù)pthread_join/pthread_tryjoin_np來回收線程的資源,并且獲得線程結(jié)束后需要返回的數(shù)據(jù)。
線程退出 #include void pthread_exit(void *retval);
? 該函數(shù)為線程退出函數(shù),在退出時候可以傳遞一個void*類型的數(shù)據(jù)帶給主線程,若選擇不傳出數(shù)據(jù),可將參數(shù)填充為NULL。
線程資源回收(阻塞) #include int pthread_join(pthread_t thread, void **retval); 成功:返回0
? 該函數(shù)為線程回收函數(shù),默認(rèn)狀態(tài)為阻塞狀態(tài),直到成功回收線程后被沖開阻塞。第一個參數(shù)為要回收線程的tid號,第二個參數(shù)為線程回收后接受線程傳出的數(shù)據(jù)。
線程資源回收(非阻塞) #define _GNU_SOURCE #include int pthread_tryjoin_np(pthread_t thread, void **retval); 成功:返回0
? 該函數(shù)為非阻塞模式回收函數(shù),通過返回值判斷是否回收掉線程,成功回收則返回0,其余參數(shù)與pthread_join一致。
線程退出(指定線程號) #include int pthread_cancel(pthread_t thread); 成功:返回0
? 該函數(shù)傳入一個tid號,會強(qiáng)制退出該tid所指向的線程,若成功執(zhí)行會返回0。
? 上述描述簡單的介紹了有關(guān)線程回收的API,下面通過例程來說明上述API。
測試?yán)?:(Phtread_txex6.c)
1 #include 2 #include 3 #include 4 #include 5 6 void *fun1(void *arg) 7 { 8 static int tmp = 0;//必須要static修飾,否則pthread_join無法獲取到正確值 9 //int tmp = 0; 10 tmp = *(int *)arg; 11 tmp+=100; 12 printf("%s:Addr = %p tmp = %dn",__FUNCTION__,&tmp,tmp); 13 pthread_exit((void *)&tmp);//將變量tmp取地址轉(zhuǎn)化為void*類型傳出 14 } 15 16 17 int main() 18 { 19 20 pthread_t tid1; 21 int a = 50; 22 void *Tmp = NULL;//因pthread_join第二個參數(shù)為void**類型 23 int ret = pthread_create(&tid1,NULL,fun1,(void *)&a); 24 if(ret != 0){ 25 perror("pthread_create"); 26 return -1; 27 } 28 pthread_join(tid1,&Tmp); 29 printf("%s:Addr = %p Val = %dn",__FUNCTION__,Tmp,*(int *)Tmp); 30 return 0; 31 } 32
運(yùn)行結(jié)果:
? 上述例程先通過23行將變量以地址的形式傳入線程,在線程中做出了自加100的操作,當(dāng)線程退出的時候通過線程傳參,用void*類型的數(shù)據(jù)通過pthread_join接受。此例程去掉了之前加入的sleep函數(shù),原因是pthread_join函數(shù)具備阻塞的特性,直至成功收回掉線程后才會沖破阻塞,因此不需要靠考慮主線程會執(zhí)行到30行結(jié)束進(jìn)程的情況。特別要說明的是例程第8行,當(dāng)變量從線程傳出的時候,需要加static修飾,對生命周期做出延續(xù),否則無法傳出正確的變量值。
測試?yán)?:(Phtread_txex7.c)
1 #define _GNU_SOURCE 2 #include 3 #include 4 #include 5 #include 6 7 void *fun(void *arg) 8 { 9 printf("Pthread:%d Come !n",(int )(long)arg+1); 10 pthread_exit(arg); 11 } 12 13 14 int main() 15 { 16 int ret,i,flag = 0; 17 void *Tmp = NULL; 18 pthread_t tid[3]; 19 for(i = 0;i < 3;i++){ 20 ret = pthread_create(&tid[i],NULL,fun,(void *)(long)i); 21 if(ret != 0){ 22 perror("pthread_create"); 23 return -1; 24 } 25 } 26 while(1){//通過非阻塞方式收回線程,每次成功回收一個線程變量自增,直至3個線程全數(shù)回收 27 for(i = 0;i <3;i++){ 28 if(pthread_tryjoin_np(tid[i],&Tmp) == 0){ 29 printf("Pthread : %d exit !n",(int )(long )Tmp+1); 30 flag++; 31 } 32 } 33 if(flag >= 3) break; 34 } 35 return 0; 36 } 37
運(yùn)行結(jié)果:
? 例程7展示了如何使用非阻塞方式來回收線程,此外也展示了多個線程可以指向同一個回調(diào)函數(shù)的情況。例程6通過阻塞方式回收線程幾乎規(guī)定了線程回收的順序,若最先回收的線程未退出,則一直會被阻塞,導(dǎo)致后續(xù)先退出的線程無法及時的回收。
? 通過函數(shù)pthread_tryjoin_np,使用非阻塞回收,線程可以根據(jù)退出先后順序自由的進(jìn)行資源的回收。
測試?yán)?:(Phtread_txex8.c)
1 #define _GNU_SOURCE 2 #include 3 #include 4 #include 5 #include 6 7 void *fun1(void *arg) 8 { 9 printf("Pthread:1 come!n"); 10 while(1){ 11 sleep(1); 12 } 13 } 14 15 void *fun2(void *arg) 16 { 17 printf("Pthread:2 come!n"); 18 pthread_cancel((pthread_t )(long)arg);//殺死線程1,使之強(qiáng)制退出 19 pthread_exit(NULL); 20 } 21 22 int main() 23 { 24 int ret,i,flag = 0; 25 void *Tmp = NULL; 26 pthread_t tid[2]; 27 ret = pthread_create(&tid[0],NULL,fun1,NULL); 28 if(ret != 0){ 29 perror("pthread_create"); 30 return -1; 31 } 32 sleep(1); 33 ret = pthread_create(&tid[1],NULL,fun2,(void *)tid[0]);//傳輸線程1的線程號 34 if(ret != 0){ 35 perror("pthread_create"); 36 return -1; 37 } 38 while(1){//通過非阻塞方式收回線程,每次成功回收一個線程變量自增,直至2個線程全數(shù)回收 39 for(i = 0;i <2;i++){ 40 if(pthread_tryjoin_np(tid[i],NULL) == 0){ 41 printf("Pthread : %d exit !n",i+1); 42 flag++; 43 } 44 } 45 if(flag >= 2) break; 46 } 47 return 0; 48 } 49
運(yùn)行結(jié)果:
? 例程8展示了如何利用pthread_cancel函數(shù)主動的將某個線程結(jié)束。27行與33行創(chuàng)建了線程,將第一個線程的線程號傳參形式傳入了第二個線程。第一個的線程執(zhí)行死循環(huán)睡眠邏輯,理論上除非進(jìn)程結(jié)束,其永遠(yuǎn)不會結(jié)束,但在第二個線程中調(diào)用了pthread_cancel函數(shù),相當(dāng)于向該線程發(fā)送一個退出的指令,導(dǎo)致線程被退出,最終資源被非阻塞回收掉。此例程要注意第32行的sleep函數(shù),一定要確保線程1先執(zhí)行,因線程是無序執(zhí)行,故加入該睡眠函數(shù)控制順序,在本章后續(xù),會講解通過加鎖、信號量等手段來合理的控制線程的臨界資源訪問與線程執(zhí)行順序控制。
5.2 線程的控制
5.2.1 多線程編臨界資源訪問
? 當(dāng)線程在運(yùn)行過程中,去操作公共資源,如全局變量的時候,可能會發(fā)生彼此“矛盾”現(xiàn)象。例如線程1企圖想讓變量自增,而線程2企圖想要變量自減,兩個線程存在互相競爭的關(guān)系導(dǎo)致變量永遠(yuǎn)處于一個“平衡狀態(tài)”,兩個線程互相競爭,線程1得到執(zhí)行權(quán)后將變量自加,當(dāng)線程2得到執(zhí)行權(quán)后將變量自減,變量似乎永遠(yuǎn)在某個范圍內(nèi)浮動,無法到達(dá)期望數(shù)值,如例程9所示。
測試?yán)?:(Phtread_txex9.c)
1 #define _GNU_SOURCE 2 #include 3 #include 4 #include 5 #include 6 7 8 int Num = 0; 9 10 void *fun1(void *arg) 11 { 12 while(Num < 3){ 13 Num++; 14 printf("%s:Num = %dn",__FUNCTION__,Num); 15 sleep(1); 16 } 17 pthread_exit(NULL); 18 } 19 20 void *fun2(void *arg) 21 { 22 while(Num > -3){ 23 Num--; 24 printf("%s:Num = %dn",__FUNCTION__,Num); 25 sleep(1); 26 } 27 pthread_exit(NULL); 28 } 29 30 int main() 31 { 32 int ret; 33 pthread_t tid1,tid2; 34 ret = pthread_create(&tid1,NULL,fun1,NULL); 35 if(ret != 0){ 36 perror("pthread_create"); 37 return -1; 38 } 39 ret = pthread_create(&tid2,NULL,fun2,NULL); 40 if(ret != 0){ 41 perror("pthread_create"); 42 return -1; 43 } 44 pthread_join(tid1,NULL); 45 pthread_join(tid2,NULL); 46 return 0; 47 } 48
運(yùn)行結(jié)果:
? 為了解決上述對臨界資源的競爭問題,pthread線程引出了互斥鎖來解決臨界資源訪問。通過對臨界資源加鎖來保護(hù)資源只被單個線程操作,待操作結(jié)束后解鎖,其余線程才可獲得操作權(quán)。
5.2.2 互斥鎖API簡述
初始化互斥鎖 #include int pthread_mutex_init(phtread_mutex_t *mutex, const pthread_mutexattr_t *restrict attr); 成功:返回0
? 該函數(shù)作用為初始化一個互斥鎖,一般情況申請一個全局的pthread_mutex_t類型的互斥鎖變量,通過此函數(shù)完成鎖內(nèi)的初始化,第一個函數(shù)將該變量的地址傳入,第二個參數(shù)為控制互斥鎖的屬性,一般為NULL。當(dāng)函數(shù)成功后會返回0,代表初始化互斥鎖成功。當(dāng)然初始化互斥鎖也可以調(diào)用宏來快速初始化:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITALIZER;
互斥鎖加鎖(阻塞)/解鎖 #include int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex);
? lock函數(shù)與unlock函數(shù)分別為加鎖解鎖函數(shù),只需要傳入已經(jīng)初始化好的pthread_mutex_t互斥鎖變量,成功后會返回0。當(dāng)某一個線程獲得了執(zhí)行權(quán)后,執(zhí)行l(wèi)ock函數(shù),一旦加鎖成功后,其余線程遇到lock函數(shù)時候會發(fā)生阻塞,直至獲取資源的線程執(zhí)行unlock函數(shù)后,獲得第二執(zhí)行權(quán)的線程的阻塞模式被從開,同時也獲取了lock,導(dǎo)致其余線程同樣在阻塞,直至執(zhí)行unlock被解鎖。
? 特別注意的是,當(dāng)獲取lock之后,必須在邏輯處理結(jié)束后執(zhí)行unlock,否則會發(fā)生死鎖現(xiàn)象!導(dǎo)致其余線程一直處于阻塞狀態(tài),無法執(zhí)行下去。在使用互斥鎖的時候,尤其要注意使用pthread_cancel函數(shù),防止發(fā)生死鎖現(xiàn)象!
互斥鎖加鎖(非阻塞) #include int pthread_mutex_trylock(pthread_mutex_t *mutex); 成功:返回0
? 該函數(shù)同樣也是一個線程加鎖函數(shù),但該函數(shù)是非阻塞模式通過返回值來判斷是否加鎖成功,用法與上述阻塞加速函數(shù)一致。
互斥鎖銷毀 #include int pthread_mutex_destory(pthread_mutex_t *mutex); 成功:返回0
? 該函數(shù)是用于銷毀互斥鎖的,傳入互斥鎖的地址,就可以完成互斥鎖的銷毀,成功返回0。
測試?yán)?0:(Phtread_txex10.c)
1 #define _GNU_SOURCE 2 #include 3 #include 4 #include 5 #include 6 7 pthread_mutex_t mutex;//互斥鎖變量 一般申請全局變量 8 9 int Num = 0;//公共臨界變量 10 11 void *fun1(void *arg) 12 { 13 pthread_mutex_lock(&mutex);//加鎖 若有線程獲得鎖,則會阻塞 14 while(Num < 3){ 15 Num++; 16 printf("%s:Num = %dn",__FUNCTION__,Num); 17 sleep(1); 18 } 19 pthread_mutex_unlock(&mutex);//解鎖 20 pthread_exit(NULL);//線程退出 pthread_join會回收資源 21 } 22 23 void *fun2(void *arg) 24 { 25 pthread_mutex_lock(&mutex);//加鎖 若有線程獲得鎖,則會阻塞 26 while(Num > -3){ 27 Num--; 28 printf("%s:Num = %dn",__FUNCTION__,Num); 29 sleep(1); 30 } 31 pthread_mutex_unlock(&mutex);//解鎖 32 pthread_exit(NULL);//線程退出 pthread_join會回收資源 33 } 34 35 int main() 36 { 37 int ret; 38 pthread_t tid1,tid2; 39 ret = pthread_mutex_init(&mutex,NULL);//初始化互斥鎖 40 if(ret != 0){ 41 perror("pthread_mutex_init"); 42 return -1; 43 } 44 ret = pthread_create(&tid1,NULL,fun1,NULL);//創(chuàng)建線程1 45 if(ret != 0){ 46 perror("pthread_create"); 47 return -1; 48 } 49 ret = pthread_create(&tid2,NULL,fun2,NULL);//創(chuàng)建線程2 50 if(ret != 0){ 51 perror("pthread_create"); 52 return -1; 53 } 54 pthread_join(tid1,NULL);//阻塞回收線程1 55 pthread_join(tid2,NULL);//阻塞回收線程2 56 pthread_mutex_destroy(&mutex);//銷毀互斥鎖 57 return 0; 58 } 59
運(yùn)行結(jié)果:
? 上述例程通過加入互斥鎖,保證了臨界變量某一時刻只被某一線程控制,實現(xiàn)了臨界資源的控制。需要說明的是,線程加鎖在循環(huán)內(nèi)與循環(huán)外的情況。本歷程在進(jìn)入while循環(huán)前進(jìn)行了加鎖操作,在循環(huán)結(jié)束后進(jìn)行的解鎖操作,如果將加鎖解鎖全部放入while循環(huán)內(nèi),作為單核的機(jī)器,執(zhí)行結(jié)果無異,當(dāng)有多核機(jī)器執(zhí)行代碼時,可能會發(fā)生“搶鎖”現(xiàn)象,這取決于操作系統(tǒng)底層的實現(xiàn)。
5.2.3 多線程編執(zhí)行順序控制
? 解決了臨界資源的訪問,但似乎對線程的執(zhí)行順序無法得到控制,因線程都是無序執(zhí)行,之前采用sleep強(qiáng)行延時的方法勉強(qiáng)可以控制執(zhí)行順序,但此方法在實際項目情況往往是不可取的,其僅僅可解決線程創(chuàng)建的順序,當(dāng)創(chuàng)建之后執(zhí)行的順序又不會受到控制,于是便引入了信號量的概念,解決線程執(zhí)行順序。
? 例程11將展示線程的執(zhí)行的隨機(jī)性。
測試?yán)?1:(Phtread_txex11.c)
1 #define _GNU_SOURCE 2 #include 3 #include 4 #include 5 #include 6 7 void *fun1(void *arg) 8 { 9 printf("%s:Pthread Come!n",__FUNCTION__); 10 pthread_exit(NULL); 11 } 12 13 void *fun2(void *arg) 14 { 15 printf("%s:Pthread Come!n",__FUNCTION__); 16 pthread_exit(NULL); 17 } 18 19 void *fun3(void *arg) 20 { 21 printf("%s:Pthread Come!n",__FUNCTION__); 22 pthread_exit(NULL); 23 } 24 25 int main() 26 { 27 int ret; 28 pthread_t tid1,tid2,tid3; 29 ret = pthread_create(&tid1,NULL,fun1,NULL); 30 if(ret != 0){ 31 perror("pthread_create"); 32 return -1; 33 } 34 ret = pthread_create(&tid2,NULL,fun2,NULL); 35 if(ret != 0){ 36 perror("pthread_create"); 37 return -1; 38 } 39 ret = pthread_create(&tid3,NULL,fun3,NULL); 40 if(ret != 0){ 41 perror("pthread_create"); 42 return -1; 43 } 44 pthread_join(tid1,NULL); 45 pthread_join(tid2,NULL); 46 pthread_join(tid3,NULL); 47 return 0; 48 } 49
運(yùn)行結(jié)果:
? 通過上述例程可以發(fā)現(xiàn),多次執(zhí)行該函數(shù)其次序是無序的,線程之間的競爭無法控制,通過使用信號量來使得線程順序為可控的。
5.2.4 信號量API簡述
初始化信號量 #include int sem_init(sem_t *sem,int pshared,unsigned int value); 成功:返回0
? 該函數(shù)可以初始化一個信號量,第一個參數(shù)傳入sem_t類型的地址,第二個參數(shù)傳入0代表線程控制,否則為進(jìn)程控制,第三個參數(shù)表示信號量的初始值,0代表阻塞,1代表運(yùn)行。待初始化結(jié)束信號量后,若執(zhí)行成功會返回0。
信號量PV操作(阻塞) #include int sem_wait(sem_t *sem); int sem_post(sem_t *sem); 成功:返回0
? sem_wait函數(shù)作用為檢測指定信號量是否有資源可用,若無資源可用會阻塞等待,若有資源可用會自動的執(zhí)行“sem-1”的操作。所謂的“sem-1”是與上述初始化函數(shù)中第三個參數(shù)值一致,成功執(zhí)行會返回0.
? sem_post函數(shù)會釋放指定信號量的資源,執(zhí)行“sem+1”操作。
? 通過以上2個函數(shù)可以完成所謂的PV操作,即信號量的申請與釋放,完成對線程執(zhí)行順序的控制。
信號量申請資源(非阻塞) #include int sem_trywait(sem_t *sem); 成功:返回0
? 與互斥鎖一樣,此函數(shù)是控制信號量申請資源的非阻塞函數(shù),功能與sem_wait一致,唯一區(qū)別在于此函數(shù)為非阻塞。
信號量銷毀 #include int sem_destory(sem_t *sem); 成功:返回0
? 該函數(shù)為信號量銷毀函數(shù),執(zhí)行過后可將申請的信號量進(jìn)行銷毀。
測試?yán)?2:(Phtread_txex12.c)
1 #define _GNU_SOURCE 2 #include 3 #include 4 #include 5 #include 6 #include 7 8 sem_t sem1,sem2,sem3;//申請的三個信號量變量 9 10 void *fun1(void *arg) 11 { 12 sem_wait(&sem1);//因sem1本身有資源,所以不被阻塞 獲取后sem1-1 下次會會阻塞 13 printf("%s:Pthread Come!n",__FUNCTION__); 14 sem_post(&sem2);// 使得sem2獲取到資源 15 pthread_exit(NULL); 16 } 17 18 void *fun2(void *arg) 19 { 20 sem_wait(&sem2);//因sem2在初始化時無資源會被阻塞,直至14行代碼執(zhí)行 不被阻塞 sem2-1 下次會阻塞 21 printf("%s:Pthread Come!n",__FUNCTION__); 22 sem_post(&sem3);// 使得sem3獲取到資源 23 pthread_exit(NULL); 24 } 25 26 void *fun3(void *arg) 27 { 28 sem_wait(&sem3);//因sem3在初始化時無資源會被阻塞,直至22行代碼執(zhí)行 不被阻塞 sem3-1 下次會阻塞 29 printf("%s:Pthread Come!n",__FUNCTION__); 30 sem_post(&sem1);// 使得sem1獲取到資源 31 pthread_exit(NULL); 32 } 33 34 int main() 35 { 36 int ret; 37 pthread_t tid1,tid2,tid3; 38 ret = sem_init(&sem1,0,1); //初始化信號量1 并且賦予其資源 39 if(ret < 0){ 40 perror("sem_init"); 41 return -1; 42 } 43 ret = sem_init(&sem2,0,0); //初始化信號量2 讓其阻塞 44 if(ret < 0){ 45 perror("sem_init"); 46 return -1; 47 } 48 ret = sem_init(&sem3,0,0); //初始化信號3 讓其阻塞 49 if(ret < 0){ 50 perror("sem_init"); 51 return -1; 52 } 53 ret = pthread_create(&tid1,NULL,fun1,NULL);//創(chuàng)建線程1 54 if(ret != 0){ 55 perror("pthread_create"); 56 return -1; 57 } 58 ret = pthread_create(&tid2,NULL,fun2,NULL);//創(chuàng)建線程2 59 if(ret != 0){ 60 perror("pthread_create"); 61 return -1; 62 } 63 ret = pthread_create(&tid3,NULL,fun3,NULL);//創(chuàng)建線程3 64 if(ret != 0){ 65 perror("pthread_create"); 66 return -1; 67 } 68 /*回收線程資源*/ 69 pthread_join(tid1,NULL); 70 pthread_join(tid2,NULL); 71 pthread_join(tid3,NULL); 72 73 /*銷毀信號量*/ 74 sem_destroy(&sem1); 75 sem_destroy(&sem2); 76 sem_destroy(&sem3); 77 78 return 0; 79 } 80
運(yùn)行結(jié)果:
? 該例程加入了信號量的控制使得線程的執(zhí)行順序變?yōu)榭煽氐?,在初始化信號量時,將信號量1填入資源,使之不被sem_wait函數(shù)阻塞,在執(zhí)行完邏輯后使用sem_pos函數(shù)來填入即將要執(zhí)行的資源。當(dāng)執(zhí)行函數(shù)sem_wait后,會執(zhí)行sem自減操作,使下一次競爭被阻塞,直至通過sem_pos被釋放。
? 上述例程因38行初始化信號量1時候,使其默認(rèn)獲取到資源,43、48行初始化信號量2、3時候,使之沒有資源。于是在線程處理函數(shù)中,每個線程通過sem_wait函數(shù)來等待資源,發(fā)送阻塞現(xiàn)象。因信號量1初始值為有資源,故可以先執(zhí)行線程1的邏輯。待執(zhí)行完第12行sem_wait函數(shù),會導(dǎo)致sem1-1,使得下一次此線程會被阻塞。繼而執(zhí)行至14行,通過sem_post函數(shù)使sem2信號量獲取資源,從而沖破阻塞執(zhí)行線程2的邏輯…以此類推完成線程的有序控制。
5.3 總結(jié)
? 有關(guān)多線程的創(chuàng)建流程下圖所示,首先需要創(chuàng)建線程,一旦線程創(chuàng)建完成后,線程與線程之間會發(fā)生競爭執(zhí)行,搶占時間片來執(zhí)行線程邏輯。在創(chuàng)建線程時候,可以通過創(chuàng)建線程的第四個參數(shù)傳入?yún)?shù),在線程退出時亦可傳出參數(shù)被線程回收函數(shù)所回收,獲取到傳出的參數(shù)。
線程編程流程
? 當(dāng)多個線程出現(xiàn)后,會遇到同時操作臨界公共資源的問題,當(dāng)線程操作公共資源時需要對線程進(jìn)行保護(hù)加鎖,防止其與線程在此線程更改變量時同時更改變量,待邏輯執(zhí)行完畢后再次解鎖,使其余線程再度開始競爭?;コ怄i創(chuàng)建流程下圖所示。
互斥鎖編程流程
? 當(dāng)多個線程出現(xiàn)后,同時會遇到無序執(zhí)行的問題。有時候需要對線程的執(zhí)行順序做出限定,變引入了信號量,通過PV操作來控制線程的執(zhí)行順序,下圖所示。
信號量編程流程
審核編輯黃昊宇
-
Linux
+關(guān)注
關(guān)注
87文章
11292瀏覽量
209331 -
線程
+關(guān)注
關(guān)注
0文章
504瀏覽量
19675
發(fā)布評論請先 登錄
相關(guān)推薦
評論