多線程的使用

本章將分為兩大部分進(jìn)行講解,前半部分將引出線程的使用場景及基本概念,通過示例代碼來說明一個(gè)線程創(chuàng)建到退出到回收的基本流程。后半部分則會(huì)通過示例代碼來說明如何控制好線程,從臨界資源訪問與線程的執(zhí)行順序控制上引出互斥鎖、信號(hào)量的概念與使用方法。
5.1 線程的使用5.1.1 為什么要使用多線程
在編寫代碼時(shí),是否會(huì)遇到以下的場景會(huì)感覺到難以下手??
場景一:寫程序在拷貝文件時(shí),需要一邊去拷貝文件,一邊去向用戶展示拷貝文件的進(jìn)度時(shí),傳統(tǒng)做法是通過每次拷貝完成結(jié)束后去更新變量,再將變量轉(zhuǎn)化為進(jìn)度顯示出來。其中經(jīng)歷了拷貝->計(jì)算->顯示->拷貝->計(jì)算->顯示...直至拷貝結(jié)束。
這樣的程序架構(gòu)及其的低效,必須在單次拷貝結(jié)束后才可以刷新當(dāng)前拷貝進(jìn)度,若可以將進(jìn)程分支,一支單獨(dú)的解決拷貝問題,一支單獨(dú)的解決計(jì)算刷新問題,則程序效率會(huì)提升很多。
場景二:用阻塞方式去讀取數(shù)據(jù),實(shí)時(shí)需要發(fā)送數(shù)據(jù)的時(shí)候。例如在進(jìn)行串口數(shù)據(jù)傳輸或者網(wǎng)絡(luò)數(shù)據(jù)傳輸?shù)臅r(shí)候,我們往往需要雙向通信,當(dāng)設(shè)置讀取數(shù)據(jù)為阻塞模式時(shí)候,傳統(tǒng)的單線程只能等到數(shù)據(jù)接收來臨后才能沖過阻塞,再根據(jù)邏輯進(jìn)行發(fā)送。
當(dāng)我們要實(shí)現(xiàn)隨時(shí)發(fā)送、隨時(shí)接收時(shí),無法滿足我們的業(yè)務(wù)需求。若可以將進(jìn)程分支,一支單純的處理接收數(shù)據(jù)邏輯,一支單純的解決發(fā)送數(shù)據(jù)邏輯,就可以完美的實(shí)現(xiàn)功能。基于以上場景描述,多線程編程可以完美的解決上述問題。
5.1.2 線程概念
所謂線程,就是操作系統(tǒng)所能調(diào)度的最小單位。普通的進(jìn)程,只有一個(gè)線程在執(zhí)行對應(yīng)的邏輯。我們可以通過多線程編程,使一個(gè)進(jìn)程可以去執(zhí)行多個(gè)不同的任務(wù)。
相比多進(jìn)程編程而言,線程享有共享資源,即在進(jìn)程中出現(xiàn)的全局變量,每個(gè)線程都可以去訪問它,與進(jìn)程共享“4G”內(nèi)存空間,使得系統(tǒng)資源消耗減少。本章節(jié)來討論Linux下POSIX線程。
5.1.3 線程的標(biāo)識(shí)pthread_t
對于進(jìn)程而言,每一個(gè)進(jìn)程都有一個(gè)唯一對應(yīng)的PID號(hào)來表示該進(jìn)程,而對于線程而言,也有一個(gè)“類似于進(jìn)程的PID號(hào)”,名為tid,其本質(zhì)是一個(gè)pthread_t類型的變量。線程號(hào)與進(jìn)程號(hào)是表示線程和進(jìn)程的唯一標(biāo)識(shí),但是對于線程號(hào)而言,其僅僅在其所屬的進(jìn)程上下文中才有意義。
在程序中,可以通過函數(shù),pthread_self,來返回當(dāng)前線程的線程號(hào),例程1是打印線程tid號(hào)。
獲取線程號(hào)
#include?
pthread_t?pthread_self(void);
成功:返回線程號(hào)
測試?yán)?:(Phtread_txex1.c)
1?#include?
2?#include?
3
4?int?main()
5?{
6??pthread_t?tid?=?pthread_self();//獲取主線程的tid號(hào)
7??printf("tid?=?%lu\n",(unsigned?long)tid);
8? return?0;
9?}
注意:
因采用POSIX線程接口,故在要編譯的時(shí)候包含pthread庫,使用gcc編譯應(yīng)gcc xxx.c -lpthread 方可編譯多線程程序。
編譯運(yùn)行結(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)的程序中,一個(gè)進(jìn)程只有一個(gè)線程,可以通過函數(shù)pthread_create來創(chuàng)建線程。
該函數(shù)第一個(gè)參數(shù)為pthread_t類型的線程號(hào)地址,當(dāng)函數(shù)執(zhí)行成功后會(huì)指向新建線程的線程號(hào);
第二個(gè)參數(shù)表示了線程的屬性,一般傳入NULL表示默認(rèn)屬性;
第三個(gè)參數(shù)代表返回值為void*,形參為void*的函數(shù)指針,當(dāng)線程創(chuàng)建成功后,會(huì)自動(dòng)的執(zhí)行該回調(diào)函數(shù);
第四個(gè)參數(shù)則表示為向線程處理函數(shù)傳入的參數(shù),若不傳入,可用NULL填充,有關(guān)線程傳參后續(xù)小節(jié)會(huì)有詳細(xì)的說明,接下來通過一個(gè)簡單例程來使用該函數(shù)創(chuàng)建出一個(gè)線程。
測試?yán)?:(Phtread_txex2.c)
1??#include?
2??#include?
3??#include?
4??#include?
5?
6??void?*fun(void?*arg)
7??{
8???printf("pthread_New?=?%lu\n",(unsigned?long)pthread_self());//打印線程的tid號(hào)
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確實(shí)可以創(chuàng)建出來線程,主線程中執(zhí)行pthread_create后的tid指向了線程號(hào)空間,與子線程通過函數(shù)pthread_self打印出來的線程號(hào)一致。
特別說明的是,當(dāng)主線程伴隨進(jìn)程結(jié)束時(shí),所創(chuàng)建出來的線程也會(huì)立即結(jié)束,不會(huì)繼續(xù)執(zhí)行。并且創(chuàng)建出來的線程的執(zhí)行順序是隨機(jī)競爭的,并不能保證哪一個(gè)線程會(huì)先運(yùn)行。可以將上述代碼中sleep函數(shù)進(jìn)行注釋,觀察實(shí)驗(yàn)現(xiàn)象。
去掉上述代碼25行后運(yùn)行結(jié)果:

上述運(yùn)行代碼3次,其中有2次被進(jìn)程結(jié)束,無法執(zhí)行到子線程的邏輯,最后一次則執(zhí)行到了子線程邏輯后結(jié)束的進(jìn)程。
因此可以說明,線程的執(zhí)行順序不受控制,且整個(gè)進(jìn)程結(jié)束后所產(chǎn)生的線程也隨之被釋放,在后續(xù)內(nèi)容中將會(huì)描述如何控制線程執(zhí)行。
5.1.5 向線程傳入?yún)?shù)
pthread_create()的最后一個(gè)參數(shù)的為void類型的數(shù)據(jù),表示可以向線程傳遞一個(gè)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?=?%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);//創(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ù)的第四個(gè)參數(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)指針,否則可能會(huì)發(fā)生警告),在線程處理回調(diào)函數(shù)中,直接將void數(shù)據(jù)轉(zhuǎn)化為int類型即可,本質(zhì)上是在傳遞變量a的值。
上述兩種方法均可得到所要的值,但是要注意其本質(zhì),一個(gè)為地址傳遞,一個(gè)為值的傳遞。當(dāng)變量發(fā)生改變時(shí)候,傳遞地址后,該地址所對應(yīng)的變量也會(huì)發(fā)生改變,但傳入變量值的時(shí)候,即使地址指針?biāo)傅淖兞堪l(fā)生變化,但傳入的為變量值,不會(huì)受到指針的指向的影響,實(shí)際項(xiàng)目中切記兩者之間的區(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?=?%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
運(yùn)行結(jié)果:

上述例程講述了如何向線程傳遞一個(gè)參數(shù),在處理實(shí)際項(xiàng)目中,往往會(huì)遇到傳遞多個(gè)參數(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?=?%.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
運(yùn)行結(jié)果:

5.1.6 線程的退出與回收
線程的退出情況有三種:第一種是進(jìn)程結(jié)束,進(jìn)程中所有的線程也會(huì)隨之結(jié)束。第二種是通過函數(shù)pthread_exit來主動(dòng)的退出線程。第三種通過函數(shù)pthread_cancel被其他線程被動(dòng)結(jié)束。
當(dāng)線程結(jié)束后,主線程可以通過函數(shù)pthread_join/pthread_tryjoin_np來回收線程的資源,并且獲得線程結(jié)束后需要返回的數(shù)據(jù)。
線程退出
#include?
void?pthread_exit(void?*retval);
該函數(shù)為線程退出函數(shù),在退出時(shí)候可以傳遞一個(gè)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),直到成功回收線程后被沖開阻塞。第一個(gè)參數(shù)為要回收線程的tid號(hào),第二個(gè)參數(shù)為線程回收后接受線程傳出的數(shù)據(jù)。
線程資源回收(非阻塞)
#define?_GNU_SOURCE????????????
#include?
?int?pthread_tryjoin_np(pthread_t?thread,?void?**retval);
成功:返回0
該函數(shù)為非阻塞模式回收函數(shù),通過返回值判斷是否回收掉線程,成功回收則返回0,其余參數(shù)與pthread_join一致。
該函數(shù)傳入一個(gè)tid號(hào),會(huì)強(qiáng)制退出該tid所指向的線程,若成功執(zhí)行會(huì)返回0:
線程退出(指定線程號(hào))
#include?
int?pthread_cancel(pthread_t?thread);
成功:返回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?=?%d\n",__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第二個(gè)參數(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?=?%d\n",__FUNCTION__,Tmp,*(int?*)Tmp);
30??return?0;
31?}
32
運(yùn)行結(jié)果:

上述例程先通過23行將變量以地址的形式傳入線程,在線程中做出了自加100的操作,當(dāng)線程退出的時(shí)候通過線程傳參,用void*類型的數(shù)據(jù)通過pthread_join接受。
此例程去掉了之前加入的sleep函數(shù),原因是pthread_join函數(shù)具備阻塞的特性,直至成功收回掉線程后才會(huì)沖破阻塞,因此不需要靠考慮主線程會(huì)執(zhí)行到30行結(jié)束進(jìn)程的情況。
特別要說明的是例程第8行,當(dāng)變量從線程傳出的時(shí)候,需要加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){//通過非阻塞方式收回線程,每次成功回收一個(gè)線程變量自增,直至3個(gè)線程全數(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展示了如何使用非阻塞方式來回收線程,此外也展示了多個(gè)線程可以指向同一個(gè)回調(diào)函數(shù)的情況。例程6通過阻塞方式回收線程幾乎規(guī)定了線程回收的順序,若最先回收的線程未退出,則一直會(huì)被阻塞,導(dǎo)致后續(xù)先退出的線程無法及時(shí)的回收。
通過函數(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的線程號(hào)
34??if(ret?!=?0){
35???perror("pthread_create");
36???return?-1;
37??}
38??while(1){//通過非阻塞方式收回線程,每次成功回收一個(gè)線程變量自增,直至2個(gè)線程全數(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ù)主動(dòng)的將某個(gè)線程結(jié)束。27行與33行創(chuàng)建了線程,將第一個(gè)線程的線程號(hào)傳參形式傳入了第二個(gè)線程。
第一個(gè)的線程執(zhí)行死循環(huán)睡眠邏輯,理論上除非進(jìn)程結(jié)束,其永遠(yuǎn)不會(huì)結(jié)束,但在第二個(gè)線程中調(diào)用了pthread_cancel函數(shù),相當(dāng)于向該線程發(fā)送一個(gè)退出的指令,導(dǎo)致線程被退出,最終資源被非阻塞回收掉。
此例程要注意第32行的sleep函數(shù),一定要確保線程1先執(zhí)行,因線程是無序執(zhí)行,故加入該睡眠函數(shù)控制順序,在本章后續(xù),會(huì)講解通過加鎖、信號(hào)量等手段來合理的控制線程的臨界資源訪問與線程執(zhí)行順序控制。
推薦閱讀:專輯|Linux文章匯總專輯|程序人生專輯|C語言
微信掃描二維碼,關(guān)注我的公眾號(hào)
