第五章 線程編程
本章將分爲兩大部分進行講解,前半部分將引出線程的使用場景及基本概念,通過示例代碼來說明一個線程創建到退出到回收的基本流程。後半部分則會通過示例代碼來說明如果控制好線程,從臨界資源訪問與線程的執行順序控制上引出互斥鎖、信號量的概念與使用方法。
5.1 線程的使用
5.1.1 爲什麼要使用多線程
在編寫代碼時,是否會遇到以下的場景會感覺到難以下手?
場景一:寫程序在拷貝文件時,需要一邊去拷貝文件,一邊去向用戶展示拷貝文件的進度時,傳統做法是通過每次拷貝完成結束後去更新變量,再將變量轉化爲進度顯示出來。其中經歷了拷貝->計算->顯示->拷貝->計算->顯示…直至拷貝結束。這樣的程序架構及其的低效,必須在單次拷貝結束後纔可以刷新當前拷貝進度,若可以將進程分支,一支單獨的解決拷貝問題,一支單獨的解決計算刷新問題,則程序效率會提升很多。
場景二:用阻塞方式去讀取數據,實時需要發送數據的時候。例如在進行串口數據傳輸或者網絡數據傳輸的時候,我們往往需要雙向通信,當設置讀取數據爲阻塞模式時候,傳統的單線程只能等到數據接收來臨後才能衝過阻塞,再根據邏輯進行發送。當我們要實現隨時發送、隨時接收時,無法滿足我們的業務需求。若可以將進程分支,一支單純的處理接收數據邏輯,一支單純的解決發送數據邏輯,就可以完美的實現功能。
基於以上場景描述,多線程編程可以完美的解決上述問題。
5.1.2 線程概念
所謂線程,就是操作系統所能調度的最小單位。普通的進程,只有一個線程在執行對應的邏輯。我們可以通過多線程編程,使一個進程可以去執行多個不同的任務。相比多進程編程而言,線程享有共享資源,即在進程中出現的全局變量,每個線程都可以去訪問它,與進程共享“4G”內存空間,使得系統資源消耗減少。本章節來討論Linux下POSIX線程。
5.1.3 線程的標識pthread_t
對於進程而言,每一個進程都有一個唯一對應的PID號來表示該進程,而對於線程而言,也有一個“類似於進程的PID號”,名爲tid,其本質是一個pthread_t類型的變量。線程號與進程號是表示線程和進程的唯一標識,但是對於線程號而言,其僅僅在其所屬的進程上下文中才有意義。
在程序中,可以通過函數,pthread_self,來返回當前線程的線程號,例程1給出了打印線程tid號。
獲取線程號
#include <pthread.h>
pthread_t pthread_self(void);
成功:返回線程號
測試例程1:(Phtread_txex1.c)
1 #include <pthread.h>
2 #include <stdio.h>
3
4 int main()
5 {
6 pthread_t tid = pthread_self();//獲取主線程的tid號
7 printf("tid = %lu\n",(unsigned long)tid);
8 return 0;
9 }
注意:因採用POSIX線程接口,故在要編譯的時候包含pthread庫,使用gcc編譯應gcc xxx.c -lpthread 方可編譯多線程程序。
編譯結果:
5.1.4 線程的創建
創建線程
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
成功:返回0
在傳統的程序中,一個進程只有一個線程,可以通過函數pthread_create來創建線程。
該函數第一個參數爲pthread_t類型的線程號地址,當函數執行成功後會指向新建線程的線程號;第二個參數表示了線程的屬性,一般傳入NULL表示默認屬性;第三個參數代表返回值爲void*,形參爲void*的函數指針,當線程創建成功後,會自動的執行該回調函數;第四個參數則表示爲向線程處理函數傳入的參數,若不傳入,可用NULL填充,有關線程傳參後續小節會有詳細的說明,接下來通過一個簡單例程來使用該函數創建出一個線程。
測試例程2:(Phtread_txex2.c)
1 #include <pthread.h>
2 #include <stdio.h>
3 #include <unistd.h>
4 #include <errno.h>
5
6 void *fun(void *arg)
7 {
8 printf("pthread_New = %lu\n",(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);//創建線程
16 if(ret != 0){
17 perror("pthread_create");
18 return -1;
19 }
20
21 /*tid_main 爲通過pthread_self獲取的線程ID,tid_new通過執行pthread_create成功後tid指向的空間*/
22 printf("tid_main = %lu tid_new = %lu \n",(unsigned long)pthread_self(),(unsigned long)tid1);
23
24 /*因線程執行順序隨機,不加sleep可能導致主線程先執行,導致進程結束,無法執行到子線程*/
25 sleep(1);
26
27 return 0;
28 }
29
運行結果:
通過pthread_create確實可以創建出來線程,主線程中執行pthread_create後的tid指向了線程號空間,與子線程通過函數pthread_self打印出來的線程號一致。
特別說明的是,當主線程伴隨進程結束時,所創建出來的線程也會立即結束,不會繼續執行。並且創建出來的線程的執行順序是隨機競爭的,並不能保證哪一個線程會先運行。可以將上述代碼中sleep函數進行註釋,觀察實驗現象。
去掉上述代碼25行後運行結果:
5.1.5向線程傳入參數
pthread_create()的最後一個參數的爲void類型的數據,表示可以向線程傳遞一個void數據類型的參數,線程的回調函數中可以獲取該參數,例程3舉例瞭如何向線程傳入變量地址與變量值。
測試例程3:(Phtread_txex3.c)
1 #include <pthread.h>
2 #include <stdio.h>
3 #include <unistd.h>
4 #include <errno.h>
5
6 void *fun1(void *arg)
7 {
8 printf("%s:arg = %d Addr = %p\n",__FUNCTION__,*(int *)arg,arg);
9 }
10
11 void *fun2(void *arg)
12 {
13 printf("%s:arg = %d Addr = %p\n",__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);//創建線程傳入變量a的地址
22 if(ret != 0){
23 perror("pthread_create");
24 return -1;
25 }
27 ret = pthread_create(&tid2,NULL,fun2,(void *)(long)a);//創建線程傳入變量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
運行結果:
本例程展示瞭如何利用線程創建函數的第四個參數向線程傳入數據,舉例瞭如何以地址的方式傳入值、以變量的方式傳入值,例程代碼的21行,是將變量a先行取地址後,再次強制類型轉化爲void後傳入線程,線程處理的回調函數中,先將萬能指針void轉化爲int*,再次取地址就可以獲得該地址變量的值,其本質在於地址的傳遞。例程代碼的27行,直接將int類型的變量強制轉化爲void進行傳遞(針對不同位數機器,指針對其字數不同,需要int轉化爲long在轉指針,否則可能會發生警告),在線程處理回調函數中,直接將void數據轉化爲int類型即可,本質上是在傳遞變量a的值。
上述兩種方法均可得到所要的值,但是要注意其本質,一個爲地址傳遞,一個爲值的傳遞。當變量發生改變時候,傳遞地址後,該地址所對應的變量也會發生改變,但傳入變量值的時候,即使地址指針所指的變量發生變化,但傳入的爲變量值,不會受到指針的指向的影響,實際項目中切記兩者之間的區別。具體說明見例程4.
測試例程4:(Phtread_txex4.c)
1 #include <pthread.h>
2 #include <stdio.h>
3 #include <unistd.h>
4 #include <errno.h>
5
6 void *fun1(void *arg)
7 {
8 while(1){
9
10 printf("%s:arg = %d Addr = %p\n",__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 = %p\n",__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
運行結果:
上述例程講述瞭如何向線程傳遞一個參數,在處理實際項目中,往往會遇到傳遞多個參數的問題,我們可以通過結構體來進行傳遞,解決此問題。
測試例程5:(Phtread_txex5.c)
1 #include <pthread.h>
2 #include <stdio.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <errno.h>
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 = %.2f\n",__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 = %.2f\n",__FUNCTION__,stu.Id,stu.Name,stu.Mark);
35 sleep(1);
36 return 0;
37 }
38
運行結果:
5.1.6線程的退出與回收
線程的退出情況有三種:第一種是進程結束,進程中所有的線程也會隨之結束。第二種是通過函數pthread_exit來主動的退出線程。第三種通過函數pthread_cancel被其他線程被動結束。當線程結束後,主線程可以通過函數pthread_join/pthread_tryjoin_np來回收線程的資源,並且獲得線程結束後需要返回的數據。
線程退出
#include <pthread.h>
void pthread_exit(void *retval);
該函數爲線程退出函數,在退出時候可以傳遞一個void*類型的數據帶給主線程,若選擇不傳出數據,可將參數填充爲NULL。
線程資源回收(阻塞)
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
成功:返回0
該函數爲線程回收函數,默認狀態爲阻塞狀態,直到成功回收線程後被衝開阻塞。第一個參數爲要回收線程的tid號,第二個參數爲線程回收後接受線程傳出的數據。
線程資源回收(非阻塞)
#define _GNU_SOURCE
#include <pthread.h>
int pthread_tryjoin_np(pthread_t thread, void **retval);
成功:返回0
該函數爲非阻塞模式回收函數,通過返回值判斷是否回收掉線程,成功回收則返回0,其餘參數與pthread_join一致。
該函數傳入一個tid號,會強制退出該tid所指向的線程,若成功執行會返回0。
線程退出(指定線程號)
#include <pthread.h>
int pthread_cancel(pthread_t thread);
成功:返回0
上述描述簡單的介紹了有關線程回收的API,下面通過例程來說明上述API。
測試例程6:(Phtread_txex6.c)
1 #include <pthread.h>
2 #include <stdio.h>
3 #include <unistd.h>
4 #include <errno.h>
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 = %d\n",__FUNCTION__,&tmp,tmp);
13 pthread_exit((void *)&tmp);//將變量tmp取地址轉化爲void*類型傳出
14 }
15
16
17 int main()
18 {
19
20 pthread_t tid1;
21 int a = 50;
22 void *Tmp = NULL;//因pthread_join第二個參數爲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 = %d\n",__FUNCTION__,Tmp,*(int *)Tmp);
30 return 0;
31 }
32
運行結果:
上述例程先通過23行將變量以地址的形式傳入線程,在線程中做出了自加100的操作,當線程退出的時候通過線程傳參,用void*類型的數據通過pthread_join接受。此例程去掉了之前加入的sleep函數,原因是pthread_join函數具備阻塞的特性,直至成功收回掉線程後纔會衝破阻塞,因此不需要靠考慮主線程會執行到30行結束進程的情況。特別要說明的是例程第8行,當變量從線程傳出的時候,需要加static修飾,對生命週期做出延續,否則無法傳出正確的變量值。
測試例程7:(Phtread_txex7.c)
1 #define _GNU_SOURCE
2 #include <pthread.h>
3 #include <stdio.h>
4 #include <unistd.h>
5 #include <errno.h>
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個線程全數回收
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
運行結果:
例程7展示瞭如何使用非阻塞方式來回收線程,此外也展示了多個線程可以指向同一個回調函數的情況。例程6通過阻塞方式回收線程幾乎規定了線程回收的順序,若最先回收的線程未退出,則一直會被阻塞,導致後續先退出的線程無法及時的回收。
通過函數pthread_tryjoin_np,使用非阻塞回收,線程可以根據退出先後順序自由的進行資源的回收。
測試例程8:(Phtread_txex8.c)
1 #define _GNU_SOURCE
2 #include <pthread.h>
3 #include <stdio.h>
4 #include <unistd.h>
5 #include <errno.h>
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,使之強制退出
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個線程全數回收
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
運行結果:
例程8展示瞭如何利用pthread_cancel函數主動的將某個線程結束。27行與33行創建了線程,將第一個線程的線程號傳參形式傳入了第二個線程。第一個的線程執行死循環睡眠邏輯,理論上除非進程結束,其永遠不會結束,但在第二個線程中調用了pthread_cancel函數,相當於向該線程發送一個退出的指令,導致線程被退出,最終資源被非阻塞回收掉。此例程要注意第32行的sleep函數,一定要確保線程1先執行,因線程是無序執行,故加入該睡眠函數控制順序,在本章後續,會講解通過加鎖、信號量等手段來合理的控制線程的臨界資源訪問與線程執行順序控制。
本文未完待續,還有多線程編程臨界資源訪問,互斥鎖,多線程編程執行順序控制,信號量等內容;
由於篇幅關係,這裏只能分享這麼多。
全文下載