0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

一文帶你徹底搞懂多線程中各個難點

GReq_mcu168 ? 來源:CSDN技術(shù)社區(qū) ? 作者:903419 ? 2021-06-30 15:00 ? 次閱讀

1.什么是線程?

linux內(nèi)核中是沒有線程這個概念的,而是輕量級進程的概念:LWP。一般我們所說的線程概念是C庫當中的概念。

1.1線程是怎樣描述的?

線程實際上也是一個task_struct,工作線程拷貝主線程的task_struct,然后共用主線程的mm_struct。線程ID是在用task_struct中pid描述的,而task_struct中tgid是線程組ID,表示線程屬于該線程組,對于主線程而言,其pid和tgid是相同的,我們一般看到的進程ID就是tgid。

即:

但是獲取該gettid系統(tǒng)調(diào)用接口并沒有被封裝起來,如果確實需要獲取線程ID,可使用:

#include 《sys/syscall.h》

int TID = syscall(SYS_gettid);

則對線程組而言,所有的tgid一定是一樣的,所有的pid一定是不一樣的。主線程pid和tgid一樣,工作線程pid和tgid一定不一樣。

1.2如何查看一個線程的ID

命令:ps -eLf

上述polkitd進程是多線程的,進程ID為731,進程內(nèi)有6個線程,線程ID為731,764,765,768,781,791。

1.3多線程如何避免調(diào)用?;靵y的問題?

工作線程和主線程共用一個mm_struct,如果都向棧中壓棧,必然會導致調(diào)用棧出錯。

實際上工作線程壓棧是壓了共享區(qū),該共享區(qū)包含了許多線程獨有的資源。如圖:

每一個線程,默認在共享區(qū)中占有的空間為8M,可以使用ulimit -s修改。

進程是資源分配的基本單位,線程是調(diào)度的基本單位。

1.3.1線程獨有資源

線程ID

一組寄存器

errno

信號屏蔽字

調(diào)度優(yōu)先級

1.3.2線程共享資源和環(huán)境

文件描述符表

信號的處理方式

當前工作目錄

用戶id和組id

1.4為什么要有多線程?

舉個生活中的例子, 這就好比去銀行辦理業(yè)務。到達銀行后, 首先取一個號碼, 然后坐下來安心等待。這時候你一定希望, 辦理業(yè)務的窗口越多越好。如果把整個營業(yè)大廳當成一個進程的話, 那么每一個窗口就是一個工作線程。

1.4.1線程帶來的優(yōu)勢

1、線程會共享內(nèi)存地址空間。

2、創(chuàng)建線程花費的時間要少于創(chuàng)建進程花費的時間。

3、終止線程花費的時間要少于終止進程花費的時間。

4、線程之間上下文切換的開銷, 要小于進程之間的上下文切換。

5、線程之間數(shù)據(jù)的共享比進程之間的共享要簡單。

6、充分利用多處理器的可并行數(shù)量。(線程會提高運行效率,但當線程多到一定程度后,可能會導致效率下降,因為會有線程調(diào)度切換。)

1.4.2線程帶來的缺點

健壯性降低:多個線程之中, 只要有一個線程不夠健壯存在bug(如訪問了非法地址引發(fā)的段錯誤) , 就會導致進程內(nèi)的所有線程一起完蛋。

線程模型作為一種并發(fā)的編程模型, 效率并沒有想象的那么高, 會出現(xiàn)復雜度高、 易出錯、 難以測試和定位的問題。

1.5注意

1、并不是只有主線程才能創(chuàng)建線程, 被創(chuàng)建出來的線程同樣可以創(chuàng)建線程。

2、不存在類似于fork函數(shù)那樣的父子關(guān)系, 大家都歸屬于同一個線程組, 進程ID都相等, group_leader都指向主線程, 而且各有各的線程ID。

通過group_leader指針, 每個線程都能找到主線程。主線程存在一個鏈表頭,后面創(chuàng)建的每一個線程都會鏈入到該雙向鏈表中。

3、并非只有主線程才能調(diào)用pthread_join連接其他線程, 同一線程組內(nèi)的任意線程都可以對某線程執(zhí)行pthread_join函數(shù)。

4、并非只有主線程才能調(diào)用pthread_detach函數(shù), 其實任意線程都可以對同一線程組內(nèi)的線程執(zhí)行分離操作。

線程的對等關(guān)系:

2.線程創(chuàng)建

接口:int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);

參數(shù)解釋

1、thread:線程標識符,是一個出參

2、attr:線程屬性

3、star_routine:函數(shù)指針,保存線程入口函數(shù)的地址

4、arg:給線程入口函數(shù)傳參

返回值:成功返回0,失敗返回error number

詳解:

第一個參數(shù)是pthread_t類型的指針, 線程創(chuàng)建成功的話,會將分配的線程ID填入該指針指向的地址。線程的后續(xù)操作將使用該值作為線程的唯一標識。

第二個參數(shù)是pthread_attr_t類型, 通過該參數(shù)可以定制線程的屬性, 比如可以指定新建線程棧的大小、 調(diào)度策略等。如果創(chuàng)建線程無特殊的要求, 該值也可以是NULL, 表示采用默認屬性。

第三個參數(shù)是線程需要執(zhí)行的函數(shù)。創(chuàng)建線程, 是為了讓線程執(zhí)行一定的任務。線程創(chuàng)建成功之后, 該線程就會執(zhí)行start_routine函數(shù), 該函數(shù)之于線程, 就如同main函數(shù)之于主線程。

第四個參數(shù)是新建線程執(zhí)行的start_routine函數(shù)的入?yún)ⅰ?/p>

2.1傳入?yún)?shù)arg的選擇

不要使用臨時變量傳參,使用堆上開辟的變量可以。

例:

#include 《stdio.h》

#include 《stdlib.h》

#include 《pthread.h》

#include 《unistd.h》

void *ThreadWork(void *arg)

{

int *p = (int*)arg;

printf(“i am work thread:%p, data:%d

”,pthread_self(),*p);

pthread_exit(NULL);

}

int main()

{

int i = 1;

pthread_t tid;

int ret = pthread_create(&tid,NULL,ThreadWork,(void*)&i);//不要傳臨時變量,這里是示范

if(ret != 0)

{

perror(“pthread_create”);

return -1;

}

while(1)

{

printf(“i am main work thread

”);

sleep(1);

}

return 0;

}

2.2線程ID以及進程地址空間

線程獲取自身的ID:

#include 《pthread.h》

pthread_t pthread_self(void);

判斷兩個線程ID是否對應著同一個線程:

#include 《pthread.h》

int pthread_equal(pthread_t t1, pthread_t t2);

返回為0時,則表示兩個線程為同一個線程,非0時,表示不是同一個線程。

用戶調(diào)用pthread_create函數(shù)時, 首先要為線程分配線程棧, 而線程棧的位置就落在共享區(qū)。調(diào)用mmap函數(shù)為線程分配??臻g。pthread_create函數(shù)分配的pthread_t類型的線程ID, 不過是分配出來的空間里的一個地址, 更確切地說是一個結(jié)構(gòu)體的指針。

2.3線程注意點

1、線程ID是進程地址空間內(nèi)的一個地址, 要在同一個線程組內(nèi)進行線程之間的比較才有意義。不同線程組內(nèi)的兩個線程, 哪怕兩者的pthread_t值是一樣的, 也不是同一個線程。

2、線程ID就有可能會被復用:

1、線程退出。

2、線程組的其他線程對該線程執(zhí)行了pthread_join, 或者線程退出前將分離狀態(tài)設置為已分離。

3、再次調(diào)用pthread_create創(chuàng)建線程。

2.4線程創(chuàng)建出來的默認值

線程創(chuàng)建的第二個參數(shù)是pthread_attr_t類型的指針, pthread_attr_init函數(shù)會將線程的屬性重置成默認值。

如果確實需要很多的線程, 可以調(diào)用接口來調(diào)整線程棧的大?。?/p>

#include 《pthread.h》

int pthread_attr_setstacksize(pthread_attr_t *attr,size_t stacksize);

int pthread_attr_getstacksize(pthread_attr_t *attr,size_t *stacksize);

3.線程終止

線程終止,但進程不會終止的方法:

1、入口函數(shù)的return返回,線程就退出了

2、線程調(diào)用pthread_exit(NULL),誰調(diào)用誰退出

#include 《pthread.h》

void pthread_exit(void *retval);

參數(shù):retval是返回信息,”臨終遺言“,可以給可以不給

該變量不能使用臨時變量。

可使用:全局變量、堆上開辟的空間、字符串常量。

pthread_exit和線程啟動函數(shù)(start_routine) 執(zhí)行return是有區(qū)別的。在start_routine中調(diào)用的任何層級的函數(shù)執(zhí)行pthread_exit() 都會引發(fā)線程退出, 而return, 只能是在start_routine函數(shù)內(nèi)執(zhí)行才能導致線程退出。

3、其它線程調(diào)用了pthread_cancel函數(shù)取消了該線程

int pthread_cancel(pthread_t thread);

thread:線程標識符

調(diào)用該函數(shù)的執(zhí)行流可以取消其它線程,但是需要知道其它線程的線程標識符,也可以執(zhí)行流自己取消自己,傳入自己的線程標識符。

如果線程組中的任何一個線程調(diào)用了exit函數(shù), 或者主線程在main函數(shù)中執(zhí)行了return語句, 那么整個線程組內(nèi)的所有線程都會終止。

4.線程等待

4.1線程等待接口

#include 《pthread.h》

int pthread_join(pthread_t thread, void **retval);

調(diào)用該函數(shù),該執(zhí)行流在等待線程退出的時候,該執(zhí)行流是阻塞在pthread_joind當中的。

4.2線程等待和進程等待的不同

第一點不同之處是進程之間的等待只能是父進程等待子進程, 而線程則不然。線程組內(nèi)的成員是對等的關(guān)系, 只要是在一個線程組內(nèi), 就可以對另外一個線程執(zhí)行連接(join) 操作。

第二點不同之處是進程可以等待任一子進程的退出 , 但是線程的連接操作沒有類似的接口, 即不能連接線程組內(nèi)的任一線程, 必須明確指明要連接的線程的線程ID。

4.3為什么要等待退出的線程?

如果不連接已經(jīng)退出的線程, 會導致資源無法釋放。所謂資源指的又是什么呢?

1、已經(jīng)退出的線程, 其空間沒有被釋放, 仍然在進程的地址空間之內(nèi)。

2、新創(chuàng)建的線程, 沒有復用剛才退出的線程的地址空間。

如果不執(zhí)行連接操作, 線程的資源就不能被釋放, 也不能被復用, 這就造成了資源的泄漏。

縱然調(diào)用了pthread_join, 也并沒有立即調(diào)用munmap來釋放掉退出線程的棧, 它們是被后建的線程復用了。釋放線程資源的時候, 若進程可能再次創(chuàng)建線程, 而頻繁地munmap和mmap會影響性能, 所以將該棧緩存起來, 放到一個鏈表之中, 如果有新的創(chuàng)建線程的請求, 會首先在棧緩存鏈表中尋找空間合適的棧, 有的話, 直接將該棧分配給新創(chuàng)建的線程。

例:

#include 《stdio.h》

#include 《stdlib.h》

#include 《pthread.h》

#include 《unistd.h》

#include 《sys/syscall.h》

void *ThreadWork(void *arg)

{

int *p = (int*)arg;

printf(“pid : %d

”,syscall(SYS_gettid));

printf(“i am work thread:%p, data:%d

”,pthread_self(),*p);

sleep(3);

pthread_exit(NULL);

}

int main()

{

int i = 1;

pthread_t tid;

int ret = pthread_create(&tid,NULL,ThreadWork,(void*)&i);//不要傳臨時變量,這里是示范

if(ret != 0)

{

perror(“pthread_create”);

return -1;

}

pthread_join(tid,NULL);//線程等待

while(1)

{

printf(“i am main work thread

”);

sleep(1);

}

return 0;

}

5.線程分離

接口:#include 《pthread.h》

int pthread_detach(pthread_t thread);

默認情況下, 新創(chuàng)建的線程處于可連接(Joinable) 的狀態(tài), 可連接狀態(tài)的線程退出后, 需要對其執(zhí)行連接操作, 否則線程資源無法釋放, 從而造成資源泄漏。

如果其他線程并不關(guān)心線程的返回值, 那么連接操作就會變成一種負擔:你不需要它, 但是你不去執(zhí)行連接操作又會造成資源泄漏。這時候你需要的東西只是:線程退出時, 系統(tǒng)自動將線程相關(guān)的資源釋放掉, 無須等待連接。

可以是線程組內(nèi)其他線程對目標線程進行分離, 也可以是線程自己執(zhí)行pthread_detach函數(shù)。

線程的狀態(tài)之中, 可連接狀態(tài)和已分離狀態(tài)是沖突的, 一個線程不能既是可連接的, 又是已分離的。因此, 如果線程處于已分離的狀態(tài), 其他線程嘗試連接線程時, 會返回EINVAL錯誤。

注意:這里的已分離不是指線程失去控制,不歸線程組管,而是指線程退出后,系統(tǒng)會自動釋放線程資源。若是線程組內(nèi)的任意線程執(zhí)行了exit函數(shù),即使是已分離的線程,也仍會收到影響,一并退出。

6.線程安全

線程安全中涉及到的概念:

臨界資源:多線程中都能訪問到的資源

臨界區(qū):每個線程內(nèi)部,訪問臨界資源的代碼,就叫臨界區(qū)

6.1什么是線程不安全?

多個線程訪問同一塊臨界資源,導致資源產(chǎn)生二義性的現(xiàn)象。

6.1.1舉一個例子

假設現(xiàn)在有兩個線程A和B,單核CPU的情況下,此時有一個int類型的全局變量為100,A和B的入口函數(shù)都要對這個全局變量進行–操作。

線程A先拿到CPU資源后,對全局變量進行–操作并不是原子性操作,也就是意味著,A在執(zhí)行–的過程中有可能會被打斷。假設A剛剛將全局變量的值讀到寄存器當中,就被切換出去了,此時程序計數(shù)器保存了下一條執(zhí)行的指令,上下文信息保存寄存器中的值,這兩個東西是用來線程A再次拿到CPU資源后,恢復現(xiàn)場使用的。

此時,線程B拿到了CPU資源,對全局變量進行了–操作,并且將100減為了99,回寫到了內(nèi)存中。

A再次擁有了CPU資源后,恢復現(xiàn)場,繼續(xù)往下執(zhí)行,從寄存器中讀到的值仍為100,減完之后為99,回寫到內(nèi)存中為99。

上述例子中,線程A和B都對全局變量進行了–操作,全局變量的值應該變?yōu)?8,但程序現(xiàn)在實際的結(jié)果為99,所以這就導致了線程不安全。

6.2如何解決線程不安全現(xiàn)象?

解決方案只需做到下述三點即可:

1、代碼必須要有互斥的行為:當一個線程正在臨界區(qū)中執(zhí)行時, 不允許其他線程進入該臨界區(qū)中。

2、如果多個線程同時要求執(zhí)行臨界區(qū)的代碼, 并且當前臨界區(qū)并沒有線程在執(zhí)行, 那么只能允許一個線程進入該臨界區(qū)。

3、如果線程不在臨界區(qū)中執(zhí)行, 那么該線程不能阻止其他線程進入臨界區(qū)。

鎖是一個很普遍的需求, 當然用戶可以自行實現(xiàn)鎖來保護臨界區(qū)。但是實現(xiàn)一個正確并且高效的鎖非常困難??v然拋下高效不談, 讓用戶從零開始實現(xiàn)一個正確的鎖也并不容易。正是因為這種需求具有普遍性, 所以Linux提供了互斥量。

6.3互斥量接口

6.3.1互斥量的初始化

1、靜態(tài)分配:

#include 《pthread.h》

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

2、動態(tài)分配:

int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);

調(diào)用int pthread_mutex_init()函數(shù)后,互斥量是處于沒有加鎖的狀態(tài)。

6.3.2互斥量的銷毀

int pthread_mutex_destroy(pthread_mutex_t *mutex);

注意:

1、使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量無須銷毀。

2、不要銷毀一個已加鎖的互斥量, 或者是真正配合條件變量使用的互斥量。

3、已經(jīng)銷毀的互斥量, 要確保后面不會有線程再嘗試加鎖。

當互斥量處于已加鎖的狀態(tài), 或者正在和條件變量配合使用, 調(diào)用pthread_mutex_destroy函數(shù)會返回EBUSY錯誤碼。

6.3.3互斥量的加鎖

int pthread_mutex_lock(pthread_mutex_t *mutex);

int pthread_mutex_trylock(pthread_mutex_t *mutex);

int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout);

第一個接口:int pthread_mutex_lock(pthread_mutex_t *mutex);

1、該接口是阻塞加鎖接口。

2、mutex為傳入互斥鎖變量的地址

3、如果mutex當中的計數(shù)器為1,pthread_mutex_lock接口就返回了,表示加鎖成功,同時計數(shù)器當中的值會被更改為0.

4、如果mutex當中的計數(shù)器為0,pthread_mutex_lock接口就阻塞了,pthread_mutex_lock接口沒有返回了,阻塞在函數(shù)內(nèi)部,直到加鎖成功

第二個接口:int pthread_mutex_trylock(pthread_mutex_t *mutex);

1、該接口為非阻塞接口

2、mutex中計數(shù)器為1時,加鎖成功,計數(shù)器置為0,然后返回

3、mutex中計數(shù)器為0時,加鎖失敗,但也會返回,此時加鎖是失敗狀態(tài),一定不要去訪問臨界資源

4、非阻塞接口一般都需要搭配循環(huán)來使用。

第三個接口:int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout);

1、帶有超時時間的加鎖接口

2、不能直接獲取互斥鎖的時候,會等待abs_timeout時間

3、如果在這個時間內(nèi)加鎖成功了,直接返回,不需要再繼續(xù)等待剩余的時間,并且表示加鎖成功

4、如果超出了該時間,也返回了,但是加鎖失敗了,需要循環(huán)加鎖

上述三個加鎖接口,第一個接口用的最多。

6.3.4互斥量的解鎖

int pthread_mutex_unlock(pthread_mutex_t *mutex);

對上述所有的加鎖接口,都可使用該函數(shù)解鎖

解鎖的時候,會將互斥鎖當中計數(shù)器的值從0變?yōu)?,表示其它線程可以獲取互斥量

6.4互斥鎖的本質(zhì)

1、在互斥鎖內(nèi)部有一個計數(shù)器,其實就是互斥量,計數(shù)器的值只能為0或者為1

2、當線程獲取互斥鎖的時候,如果計數(shù)器當前值為0,表示當前線程不能獲取到互斥鎖,也就是沒有獲取到互斥鎖,就不要去訪問臨界資源

3、當前線程獲取互斥鎖的時候,如果計數(shù)器當前值為1,表示當前線程可以獲取到互斥鎖,也就是意味著可以訪問臨界資源

6.5互斥鎖中的計數(shù)器如何保證了原子性?

獲取鎖資源的時候(加鎖):

1、寄存器當中值直接賦值為0

2、將寄存器當中的值和計數(shù)器當中的值進行交換

3、判斷寄存器當中的值,得出加鎖結(jié)果

例:4個線程,對同一個全局變量進行減減操作

#include 《stdio.h》

#include 《stdlib.h》

#include 《pthread.h》

#include 《unistd.h》

#include 《sys/syscall.h》

#define NUMBER 4

int g_val = 100;

pthread_mutex_t mutex;//定義互斥鎖

void *ThreadWork(void *arg)

{

int *p = (int*)arg;

pthread_detach(pthread_self());//自己分離自己,不用主線程回收它的資源了

while(1)

{

pthread_mutex_lock(&mutex);//加鎖

if(g_val 》 0)

{

printf(“i am pid : %d,i get g_val : %d

”,(int)syscall(SYS_gettid),g_val);

--g_val;

usleep(2);

}

else{

pthread_mutex_unlock(&mutex);//在所有可能退出的地方,進行解鎖

break;

}

pthread_mutex_unlock(&mutex);//解鎖

}

pthread_exit(NULL);

}

int main()

{

pthread_t tid[NUMBER];

pthread_mutex_init(&mutex,NULL);//互斥鎖初始化

int i = 0;

for(;i 《 NUMBER;++i)

{

int ret = pthread_create(&tid[i],NULL,ThreadWork,(void*)&g_val);//不要傳臨時變量,這里是示范

if(ret != 0)

{

perror(“pthread_create”);

return -1;

}

}

//pthread_join(tid,NULL);//線程等待

//pthread_detach(tid);//線程分離

pthread_mutex_destroy(&mutex);//銷毀互斥鎖

while(1)

{

printf(“i am main work thread

”);

sleep(1);

}

return 0;

}

6.6互斥鎖公平嘛?

互斥鎖是不公平的。

內(nèi)核維護等待隊列, 互斥量實現(xiàn)了大體上的公平;由于等待線程被喚醒后, 并不自動持有互斥量, 需要和剛進入臨界區(qū)的線程競爭(搶鎖), 所以互斥量并沒有做到先來先服務。

6.7互斥鎖的類型

1、PTHREAD_MUTEX_NORMAL:最普通的一種互斥鎖。它不具備死鎖檢測功能, 如線程對自己鎖定的互斥量再次加鎖, 則會發(fā)生死鎖。

2、

PTHREAD_MUTEX_RECURSIVE_NP:支持遞歸的一種互斥鎖, 該互斥量的內(nèi)部維護有互斥鎖的所有者和一個鎖計數(shù)器。當線程第一次取到互斥鎖時, 會將鎖計數(shù)器置1, 后續(xù)同一個線程再次執(zhí)行加鎖操作時, 會遞增該鎖計數(shù)器的值。解鎖則遞減該鎖計數(shù)器的值, 直到降至0, 才會真正釋放該互斥量, 此時其他線程才能獲取到該互斥量。解鎖時, 如果互斥量的所有者不是調(diào)用解鎖的線程, 則會返回EPERM。

3、

PTHREAD_MUTEX_ERRORCHECK_NP:支持死鎖檢測的互斥鎖?;コ饬康膬?nèi)部會記錄互斥鎖的當前所有者的線程ID(調(diào)度域的線程ID) 。如果互斥量的持有線程再次調(diào)用加鎖操作, 則會返回EDEADLK。解鎖時, 如果發(fā)現(xiàn)調(diào)用解鎖操作的線程并不是互斥鎖的持有者, 則會返回EPERM。

4、自旋鎖,自旋鎖采用了和互斥量完全不同的策略, 自旋鎖加鎖失敗, 并不會讓出CPU, 而是不停地嘗試加鎖, 直到成功為止。這種機制在臨界區(qū)非常小且對臨界區(qū)的爭奪并不激烈的場景下, 效果非常好。自旋鎖的效果好, 但是副作用也大, 如果使用不當, 自旋鎖的持有者遲遲無法釋放鎖, 那么, 自旋接近于死循環(huán), 會消耗大量的CPU資源, 造成CPU使用率飆高。因此, 使用自旋鎖時, 一定要確保臨界區(qū)盡可能地小, 不要有系統(tǒng)調(diào)用, 不要調(diào)用sleep。使用strcpy/memcpy等函數(shù)也需要謹慎判斷操作內(nèi)存的大小, 以及是否會引起缺頁中斷。

5、PTHREAD_MUTEX_ADAPTIVE_NP:自適應鎖,首先與自旋鎖一樣, 持續(xù)嘗試獲取, 但過了一定時間仍然不能申請到鎖, 就放棄嘗試, 讓出CPU并等待。PTHREAD_MUTEX_ADAPTIVE_NP類型的互斥量, 采用的就是這種機制。

6.8死鎖和活鎖

線程1已經(jīng)成功拿到了互斥量1, 正在申請互斥量2, 而同時在另一個CPU上,線程2已經(jīng)拿到了互斥量2, 正在申請互斥量1。彼此占有對方正在申請的互斥量,結(jié)局就是誰也沒辦法拿到想要的互斥量, 于是死鎖就發(fā)生了。

6.8.1死鎖概念

死鎖是指在一組進程中的各個進程均占有不會釋放的資源,但因互相申請被其它進程所占有不會釋放的資源而處于一種永久等待的狀態(tài)。

6.8.2死鎖的四個必要條件

1、互斥條件:一個資源只能被一個執(zhí)行流使用

2、請求與保持條件:一個執(zhí)行流因請求資源而阻塞時,對已獲得的資源不會釋放

3、不剝奪條件:一個執(zhí)行流已獲得的資源,在未使用完之前,不能強行剝奪

4、循環(huán)等待條件:若干執(zhí)行流之間形成一種頭尾相接的循環(huán)等待資源的關(guān)系

6.8.3避免死鎖

1、破壞死鎖的四個必要條件(實際上只能破壞條件2和4)

2、加鎖順序一致(按照先后順序申請互斥鎖)

3、避免未釋放鎖的情況

4、資源一次性分配

6.8.4活鎖

避免死鎖的另一種方式是嘗試一下,如果取不到鎖就返回。

int pthread_mutex_trylock(pthread_mutex_t *mutex);

int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,const struct timespec *restrict abs_timeout);

這兩個函數(shù)反映了一種,不行就算了的思想。

trylock不行就回退的思想有可能會引發(fā)活鎖(live lock) 。生活中也經(jīng)常遇到兩個人迎面走來, 雙方都想給對方讓路, 但是讓的方向卻不協(xié)調(diào), 反而互相堵住的情況 ?;铈i現(xiàn)象與這種場景有點類似。

線程1首先申請鎖mutex_a后, 之后嘗試申請mutex_b, 失敗以后, 釋放mutex_a進入下一輪循環(huán), 同時線程2會因為嘗試申請mutex_a失敗,而釋放mutex_b, 如果兩個線程恰好一直保持這種節(jié)奏, 就可能在很長的時間內(nèi)兩者都一次次地擦肩而過。當然這畢竟不是死鎖, 終究會有一個線程同時持有兩把鎖而結(jié)束這種情況。盡管如此, 活鎖的確會降低性能。

6.8.5死鎖調(diào)試

查看多個線程堆棧:thread apply all bt

跳轉(zhuǎn)到線程中:t 線程號

查看具體的調(diào)用堆棧:f 堆棧號

直接從pid號用gdb調(diào)試:gdb attach pid

#include 《stdio.h》

#include 《stdlib.h》

#include 《pthread.h》

#include 《unistd.h》

#include 《sys/syscall.h》

#define NUMBER 2

pthread_mutex_t mutex1;//定義互斥鎖

pthread_mutex_t mutex2;

void *ThreadWork1(void *arg)

{

int *p = (int*)arg;

pthread_mutex_lock(&mutex1);

sleep(2);

pthread_mutex_lock(&mutex2);

pthread_mutex_unlock(&mutex2);

pthread_mutex_unlock(&mutex1);

return NULL;

}

void *ThreadWork2(void *arg)

{

int *p = (int*)arg;

pthread_mutex_lock(&mutex2);

sleep(2);

pthread_mutex_lock(&mutex1);

pthread_mutex_unlock(&mutex1);

pthread_mutex_unlock(&mutex2);

return NULL;

}

int main()

{

pthread_t tid[NUMBER];

pthread_mutex_init(&mutex1,NULL);//互斥鎖初始化

pthread_mutex_init(&mutex2,NULL);//互斥鎖初始化

int i = 0;

int ret = pthread_create(&tid[0],NULL,ThreadWork1,(void*)&i);

if(ret != 0)

{

perror(“pthread_create”);

return -1;

}

ret = pthread_create(&tid[1],NULL,ThreadWork2,(void*)&i);

if(ret != 0)

{

perror(“pthread_create”);

return -1;

}

//pthread_join(tid,NULL);//線程等待

//pthread_join(tid,NULL);//線程等待

//pthread_detach(tid);//線程分離

pthread_join(tid[0],NULL);

pthread_join(tid[1],NULL);

pthread_mutex_destroy(&mutex1);//銷毀互斥鎖

pthread_mutex_destroy(&mutex2);//銷毀互斥鎖

while(1)

{

printf(“i am main work thread

”);

sleep(1);

}

return 0;

}

在上述代碼中,一定會出現(xiàn)死鎖,線程1拿到了互斥鎖1,又再去申請線程2的互斥鎖2,線程2拿到了互斥鎖2又再去申請線程1的互斥鎖1。

開始調(diào)試:

1、找到進程號

2、開始調(diào)試

3、查看多個線程堆棧

4、跳轉(zhuǎn)到線程中

5、查看具體調(diào)用堆棧

6、查看互斥鎖1和互斥鎖2,分別被誰拿著

6.9讀寫鎖

6.9.1什么是讀寫鎖?

大部分情況下,對于共享變量的訪問特點:只是讀取共享變量的值,而不是修改,只有在少數(shù)情況下,才會真正的修改共享變量的值。

在這種情況下,讀請求之間是同步的,它們之間的并發(fā)訪問是安全的。然而寫請求必須鎖住讀請求和其它寫請求。

即讀線程可多個同時讀,而寫線程只允許同一時間內(nèi)一個線程去寫。

6.9.2讀寫鎖接口

#include 《pthread.h》

//銷毀

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

//初始化

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,

const pthread_rwlockattr_t *restrict attr);

對于調(diào)用pthread_rwlock_init初始化的讀寫鎖,在不需要讀寫鎖的時候,需要調(diào)用pthread_rwlock_destroy銷毀。

6.9.3讀者加鎖

#include 《pthread.h》

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); //阻塞類型的讀加鎖接口

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); //非阻塞類型的讀加鎖接口

最大的好處就是,允許多個線程以只讀加鎖的方式獲取到讀寫鎖;

本質(zhì)上,讀寫鎖的內(nèi)部維護了一個引用計數(shù),每當線程以讀方式獲取讀寫鎖時,該引用計數(shù)+1;

當釋放以讀加鎖的方式的讀寫鎖時,會先對引用計數(shù)進行-1,直到引用計數(shù)的值為0的時候,才真正釋放了這把讀寫鎖。

6.9.4寫者加鎖

#include 《pthread.h》

int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);// 非阻塞寫

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);//阻塞寫

寫鎖用的是獨占模式,如果當前讀寫鎖被某寫線程占用著,則不允許任何讀鎖通過請求,也不允許任何寫鎖請求通過,讀鎖請求和寫鎖請求都要陷入阻塞,直到線程釋放寫鎖。

6.9.5 解鎖

#include 《pthread.h》

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

不論是讀者加鎖還是寫者加鎖,都采用該接口進行解釋。

讀者解鎖,只有當引用計數(shù)為0的時候,才真正釋放了讀寫鎖。

6.9.6讀寫鎖的競爭策略

對于讀寫鎖而言,目前有兩種策略,讀者優(yōu)先和攜著優(yōu)先;

讀寫鎖的類型有如下幾種:

PTHREAD_RWLOCK_PREFER_READER_NP, //讀者優(yōu)先

PTHREAD_RWLOCK_PREFER_WRITER_NP, //很唬人, 但是也是讀者優(yōu)先

PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP, //寫者優(yōu)先

PTHREAD_RWLOCK_DEFAULT_NP = PTHREAD_RWLOCK_PREFER_READER_NP

讀者優(yōu)先:讀鎖來請求可以立即響應,只要有一個讀鎖沒完成,那么寫鎖就無法寫。這種策略是不公平的,極端情況下,寫現(xiàn)場很可能被餓死,即線程總是拿不到鎖資源。

寫者優(yōu)先:只要線程申請了寫鎖,那么在寫鎖后面到來的讀鎖請求就會統(tǒng)統(tǒng)被阻塞,不能先于寫鎖拿到鎖。

讀寫鎖實現(xiàn)中的變量及含義

對于讀請求而言:如果

1. 無線程持有寫鎖,即_writer = 0.

2. 采用讀者優(yōu)先策略或者當前沒有寫鎖申請請求,即 _nr_writers_queue = 0

3. 當滿足這兩個條件時,讀鎖請求立即獲得讀鎖,返回之前執(zhí)行_nr_readers++,表示多了一個線程正在讀

4. 不滿足這兩個條件時,執(zhí)行_nr_readers_queued++,表示增加了一個讀鎖等待者,然后調(diào)用futex,陷入阻塞。醒來之后,執(zhí)行_nr_readers_queued- -,再次判斷是否滿足條件1,2

對于寫請求而言:如果

1. 無線程持有寫鎖,即_writer = 0.

2. 沒有線程持有讀鎖,即_nr_readers = 0.

3. 如果上述條件滿足,就會立即拿到鎖,將_writer 置為當前線程的ID

4. 如果不滿足,則執(zhí)行_nr_writers_queue++, 表示增加了一個寫鎖等待者線程,然后執(zhí)行futex陷入等待。醒來后,先執(zhí)行_nr_writers_queue- -,再繼續(xù)判斷條件1,2

對于解鎖,如果當前是寫鎖:

1. 執(zhí)行_writer = 0.,表示釋放寫鎖。

2. 根據(jù)_nr_writers_queue判斷有沒有寫鎖,如果有則喚醒一個寫鎖,如果沒有寫鎖等待者,則喚醒所有的讀鎖等待者。

對于解鎖,如果當前是讀鎖:

1. 執(zhí)行_nr_readers- -,表示讀鎖占有者少了一個。

2. 判斷_nr_readers是否等于0,是的話則表示當前線程是最后一個讀鎖占有者,需要喚醒寫鎖等待者或讀鎖等待者

3. 根據(jù)_nr_writers_queue判斷是否存在寫鎖等待者,若有,則喚醒一個寫鎖等待線程

4. 如果沒有寫鎖等待者,判斷是否存在讀鎖等待者,若有,則喚醒全部的讀鎖等待者

讀寫鎖很容易造成,讀者餓死或者寫者餓死。

也可以設計公平的讀寫鎖。

代碼:

#include 《stdio.h》

#include 《stdlib.h》

#include 《pthread.h》

#include 《sys/syscall.h》

#include 《unistd.h》

#include 《fcntl.h》

#define THREADCOUNT 100

static int count = 0;

static pthread_rwlock_t lock;

void* Read(void* i)

{

while(1)

{

pthread_rwlock_rdlock(&lock);

printf(“i am 讀線程 : %d, 現(xiàn)在的count是%d

”, (int)syscall(SYS_gettid), count);

pthread_rwlock_unlock(&lock);

//sleep(1);

}

}

void* Write(void* i)

{

while(1)

{

pthread_rwlock_wrlock(&lock);

++count;

printf(“i am 寫線程 : %d, 現(xiàn)在的count是: %d

”, (int)syscall(SYS_gettid), count);

pthread_rwlock_unlock(&lock);

sleep(1);

}

}

int main()

{

//close(1);

//int fd = open(“。/dup2_result.txt”, O_CREAT | O_RDWR);

//dup2(fd, 1);

pthread_t tid[THREADCOUNT];

pthread_rwlock_init(&lock, NULL);

for(int i = 0; i 《 THREADCOUNT; ++i)

{

if(i % 2 == 0)

{

pthread_create(&tid[i], NULL, Read, (void*)&i);

}

else

{

pthread_create(&tid[i], NULL, Write, (void*)&i);

}

}

for(int i = 0; i 《 THREADCOUNT; ++i)

{

pthread_join(tid[i], NULL);

}

pthread_rwlock_destroy(&lock);

return 0;

}

上述代碼很容易觸發(fā)線程餓死。

讀餓死或者寫?zhàn)I死。

7.線程間同步

7.1為什么需要線程同步?

線程同步是為了對臨界資源訪問的合理性。

例如:

就像工廠里生產(chǎn)車間沒有原料了, 所有生產(chǎn)車間都停工了, 工人們都在車間睡覺。突然進來一批原料, 如果原料充足, 你會發(fā)廣播給所有車間, 原料來了, 快來開工吧。如果進來的原料很少, 只夠一個車間開工的, 你可能只會通知一個車間開工。

7.2如何做到線程間同步?

條件等待是線程間同步的另一種方法。

如果條件不滿足, 它能做的事情就是等待, 等到條件滿足為止。通常條件的達成, 很可能取決于另一個線程, 比如生產(chǎn)者-消費者模型。當另外一個線程發(fā)現(xiàn)條件符合的時候, 它會選擇一個時機去通知等待在這個條件上的線程。有兩種可能性, 一種是喚醒一個線程, 一種是廣播, 喚醒其他線程。

則在這個情況下,需要做到:

1、線程在條件不滿足的情況下, 主動讓出互斥量, 讓其他線程去折騰, 線程在此處等待, 等待條件的滿足;

2、一旦條件滿足, 線程就可以立刻被喚醒。

3、線程之所以可以安心等待, 依賴的是其他線程的協(xié)作, 它確信會有一個線程在發(fā)現(xiàn)條件滿足以后, 將向它發(fā)送信號, 并且讓出互斥量。

7.3條件變量

本質(zhì)上是PCB等待隊列 + 等待接口 + 喚醒接口。

7.3.1條件變量的初始化

靜態(tài)初始化

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

動態(tài)初始化

pthread_cond_init(pthread_cond_t *cond,const pthread_condattr_t *attr);

7.3.2條件變量的等待

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);

int pthread_cond_timedwait(pthread_cond_t *restrict conpthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);

為什么這兩個接口中有互斥鎖?

條件不會無緣無故地突然變得滿足了, 必然會牽扯到共享數(shù)據(jù)的變化。所以一定要有互斥鎖來保護。沒有互斥鎖, 就無法安全地獲取和修改共享數(shù)據(jù)。

同步并沒有保證互斥,而保證互斥是使用到了互斥鎖。

pthread_mutex_lock(&m)

while(condition_is_false)

{

pthread_mutex_unlock(&m);

//解鎖之后, 等待之前, 可能條件已經(jīng)滿足, 信號已經(jīng)發(fā)出, 但是該信號可能會被錯過

cond_wait(&cv);

pthread_mutex_lock(&m);

}

上面的解鎖和等待不是原子操作。解鎖以后, 調(diào)用cond_wait之前,如果已經(jīng)有其他線程獲取到了互斥量, 并且滿足了條件, 同時發(fā)出了通知信號, 那么cond_wait將錯過這個信號, 可能會導致線程永遠處于阻塞狀態(tài)。所以解鎖加等待必須是一個原子性的操作, 以確保已經(jīng)注冊到事件的等待隊列之前, 不會有其他線程可以獲得互斥量。

那先注冊等待事件, 后釋放鎖不行嗎?注意, 條件等待是個阻塞型的接口, 不單單是注冊在事件的等待隊列上, 線程也會因此阻塞于此, 從而導致互斥量無法釋放, 其他線程獲取不到互斥量, 也就無法通過改變共享數(shù)據(jù)使等待的條件得到滿足, 因此這就造成了死鎖。

pthread_mutex_lock(&m);

while(condition_is_false)

pthread_cond_wait(&v,&m);//此處會阻塞

/*如果代碼運行到此處, 則表示我們等待的條件已經(jīng)滿足了,

*并且在此持有了互斥量

*/

/*在滿足條件的情況下, 做你想做的事情。

*/

pthread_mutex_unlock(&m);

pthread_cond_wait函數(shù)只能由擁有互斥量的線程來調(diào)用, 當該函數(shù)返回的時候, 系統(tǒng)會確保該線程再次持有互斥量, 所以這個接口容易給人一種誤解, 就是該線程一直在持有互斥量。事實上并不是這樣的。這個接口向系統(tǒng)聲明了我在PCB等待序列中之后, 就把互斥量給釋放了。這樣其他線程就有機會持有互斥量,操作共享數(shù)據(jù), 觸發(fā)變化, 使線程等待的條件得到滿足。

pthread_cond_wait內(nèi)部會進行解鎖邏輯,則一定要先放到PCB等待序列中,再進行解鎖。

while(condition_is_false)

pthread_cond_wait(&v,&m);//此處會阻塞

if(condition_is_false)

pthread_cond_wait(&v,&m);//此處會阻塞

喚醒以后, 再次檢查條件是否滿足, 是不是多此一舉?

因為喚醒中存在虛假喚醒(spurious wakeup) , 換言之,條件尚未滿足, pthread_cond_wait就返了。在一些實現(xiàn)中, 即使沒有其他線程向條件變量發(fā)送信號, 等待此條件變量的線程也有可能會醒來。

條件滿足了發(fā)送信號, 但等到調(diào)用pthread_cond_wait的線程得到CPU資源時, 條件又再次不滿足了。好在無論是哪種情況, 醒來之后再次測試條件是否滿足就可以解決虛假等待的問題。

pthread_cond_wait內(nèi)部實現(xiàn)邏輯:

將調(diào)用pthread_cond_wait函數(shù)的執(zhí)行流放入到PCB等待隊列當中

解鎖

等待被喚醒

被喚醒之后:

1、從PCB等待隊列中移除出來

2、搶占互斥鎖

情況1:拿到互斥鎖,pthread_cond_wait就返回了

情況2:沒有拿到互斥鎖,阻塞在pthread_cond_wait內(nèi)部搶鎖的邏輯中

當阻塞在pthread_cond_wait函數(shù)搶鎖邏輯中時,一旦執(zhí)行流時間耗盡,意味著線程就被切換出來了,程序計數(shù)器就保存的是搶鎖的指令,上下文信息保存的就是寄存器的值

當再次擁有CPU資源后,恢復搶鎖邏輯

直到搶鎖成功,pthread_cond_wait函數(shù)才會返回

7.3.3條件變量的喚醒

int pthread_cond_signal(pthread_cond_t *cond);

int pthread_cond_broadcast(pthread_cond_t *cond);

pthread_cond_signal負責喚醒等待在條件變量上的一個線程。

pthread_cond_broadcast,就是廣播喚醒等待在條件變量上的所有線程。

先發(fā)送信號,然后解鎖互斥量,這個順序是必須的嘛?

先通知條件變量、 后解鎖互斥量, 效率會比先解鎖、 后通知條件變量低。因為先通知后解鎖, 執(zhí)行pthread_cond_wait的線程可能在互斥量已然處于加鎖狀態(tài)的時候醒來, 發(fā)現(xiàn)互斥量仍然沒有解鎖, 就會再次休眠, 從而導致了多余的上下文切換。

7.3.4條件變量的銷毀

int pthread_cond_destroy(pthread_cond_t *cond);

注意:

1、永遠不要用一個條件變量對另一個條件變量賦值, 即pthread_cond_t cond_b = cond_a不合法, 這種行為是未定義的。

2、使用PTHREAD_COND_INITIALIZE靜態(tài)初始化的條件變量, 不需要被銷毀。

3、要調(diào)用pthread_cond_destroy銷毀的條件變量可以調(diào)用pthread_cond_init重新進行初始化。

4、不要引用已經(jīng)銷毀的條件變量, 這種行為是未定義的。

例:

#include 《stdio.h》

#include 《stdlib.h》

#include 《pthread.h》

#include 《unistd.h》

#include 《sys/syscall.h》

#define NUMBER 2

int g_bowl = 0;

pthread_mutex_t mutex;//定義互斥鎖

pthread_cond_t cond1;//條件變量

pthread_cond_t cond2;//條件變量

void *WorkProduct(void *arg)

{

int *p = (int*)arg;

while(1)

{

pthread_mutex_lock(&mutex);

while(*p 》 0)

{

pthread_cond_wait(&cond2,&mutex);//條件等待,條件不滿足,陷入阻塞

}

++(*p);

printf(“i am workproduct :%d,i product %d

”,(int)syscall(SYS_gettid),*p);

pthread_cond_signal(&cond1);//通知消費者

pthread_mutex_unlock(&mutex);//釋放鎖

}

return NULL;

}

void *WorkConsume(void *arg)

{

int *p = (int*)arg;

while(1)

{

pthread_mutex_lock(&mutex);

while(*p 《= 0)

{

pthread_cond_wait(&cond1,&mutex);//條件等待,條件不滿足,陷入阻塞

}

printf(“i am workconsume :%d,i consume %d

”,(int)syscall(SYS_gettid),*p);

--(*p);

pthread_cond_signal(&cond2);//通知生產(chǎn)者

pthread_mutex_unlock(&mutex);//釋放鎖

}

return NULL;

}

int main()

{

pthread_t cons[NUMBER],prod[NUMBER];

pthread_mutex_init(&mutex,NULL);//互斥鎖初始化

pthread_cond_init(&cond1,NULL);//條件變量初始化

pthread_cond_init(&cond2,NULL);//條件變量初始化

int i = 0;

for(;i 《 NUMBER;++i)

{

int ret = pthread_create(&prod[i],NULL,WorkProduct,(void*)&g_bowl);

if(ret != 0)

{

perror(“pthread_create”);

return -1;

}

ret = pthread_create(&cons[i],NULL,WorkConsume,(void*)&g_bowl);

if(ret != 0)

{

perror(“pthread_create”);

return -1;

}

}

for(i = 0;i 《 NUMBER;++i)

{

pthread_join(cons[i],NULL);//線程等待

pthread_join(prod[i],NULL);

}

pthread_mutex_destroy(&mutex);//銷毀互斥鎖

pthread_cond_destroy(&cond1);

pthread_cond_destroy(&cond2);

while(1)

{

printf(“i am main work thread

”);

sleep(1);

}

return 0;

}

在這里為什么有兩個條件變量呢?

若所有的線程只使用一個條件變量,會導致所有線程最后都進入PCB等待隊列。

thread apply all bt查看:

7.3.5情況分析:兩個生產(chǎn)者,兩個消費者,一個PCB等待隊列

1、最開始的情況,兩個消費者搶到了鎖,此時生產(chǎn)者未生產(chǎn),則都放入PCB等待隊列中

2、一個生產(chǎn)者搶到了鎖,生產(chǎn)了一份材料,喚醒一個消費者,此時三者搶鎖,若兩個生產(chǎn)者分別先后搶到了鎖,則都進入PCB等待隊列中

3、只有一個消費者,則必會搶到鎖,消費材料,喚醒PCB等待隊列,若此時喚醒的是,消費者,則現(xiàn)在是這樣一個情況:

4、兩個消費者在外邊搶鎖,一定都會進入PCB等待隊列中

解決上述問題可采用兩種方法:

1、使用int pthread_cond_broadcast(pthread_cond_t *cond);,喚醒PCB等待隊列中所有的線程。此時所有線程都會同時執(zhí)行搶鎖邏輯,太消費資源了。此方法不妥

2、采用兩個PCB等待序列,一個放生產(chǎn)者,一個放消費者,生產(chǎn)者喚醒消費者,消費者喚醒生產(chǎn)者。

8.線程取消

8.1線程取消函數(shù)接口

int pthread_cancel(pthread_t thread);

一個線程可以通過調(diào)用該函數(shù)向另一個線程發(fā)送取消請求。這不是個阻塞型接口, 發(fā)出請求后, 函數(shù)就立刻返回了, 而不會等待目標線程退出之后才返回。

調(diào)用pthread_cancel時, 會向目標線程發(fā)送一個SIGCANCEL的信號, 該信號就是kill -l中消失的32號信號。

線程的默認取消狀態(tài)是PTHREAD_CANCEL_ENABLE。即是可被取消的。

什么是取消點?可通過man pthreads查看取消點

就是對于某些函數(shù), 如果線程允許取消且取消類型是延遲取消, 并且線程也收到了取消請求, 那么當執(zhí)行到這些函數(shù)的時候, 線程就可以退出了。

8.2線程取消帶來的弊端

目標線程可能會持有互斥量、 信號量或其他類型的鎖, 這時候如果收到取消請求, 并且取消類型是異步取消, 那么可能目標線程掌握的資源還沒有來得及釋放就被迫退出了, 這可能會給其他線程帶來不可恢復的后果, 比如死鎖(其他線程再也無法獲得資源) 。

注意:

輕易不要調(diào)用pthread_cancel函數(shù), 在外部殺死線程是很糟糕的做法,畢竟如果想通知目標線程退出, 還可以采取其他方法。

如果不得不允許線程取消, 那么在某些非常關(guān)鍵不容有失的代碼區(qū)域, 暫時將線程設置成不可取消狀態(tài), 退出關(guān)鍵區(qū)域之后, 再恢復成可以取消的狀態(tài)。

在非關(guān)鍵的區(qū)域, 也要將線程設置成延遲取消, 永遠不要設置成異步取消。

8.2線程清理函數(shù)

假設遇到取消請求, 線程執(zhí)行到了取消點, 卻沒有來得及做清理動作(如動態(tài)申請的內(nèi)存沒有釋放, 申請的互斥量沒有解鎖等) , 可能會導致錯誤的產(chǎn)生, 比如死鎖, 甚至是進程崩潰。

為了避免這種情況, 線程可以設置一個或多個清理函數(shù), 線程取消或退出時,會自動執(zhí)行這些清理函數(shù), 以確保資源處于一致的狀態(tài)。

如果線程被取消, 清理函數(shù)則會負責解鎖操作。

void pthread_cleanup_push(void (*routine)(void *),void *arg);

void pthread_cleanup_pop(int execute);

這兩個函數(shù)必須同時出現(xiàn), 并且屬于同一個語法塊。

何時會觸發(fā)注冊的清理函數(shù):?

1、當線程的主函數(shù)是調(diào)用pthread_exit返回的, 清理函數(shù)總是會被執(zhí)行。

2、當線程是被其他線程調(diào)用pthread_cancel取消的, 清理函數(shù)總是會被執(zhí)行。

3、當線程的主函數(shù)是通過return返回的, 并且pthread_cleanup_pop的唯一參數(shù)execute是0時, 清理函數(shù)不會被執(zhí)行。

4、線程的主函數(shù)是通過return返回的, 并且pthread_cleanup_pop的唯一參數(shù)execute是非零值時, 清理函數(shù)會執(zhí)行一次。

代碼:

#include 《stdio.h》

#include 《stdlib.h》

#include 《time.h》

#include 《pthread.h》

#include 《unistd.h》

#include 《sys/syscall.h》

#define NUMBER 2

int g_bowl = 0;

pthread_mutex_t mutex;//定義互斥鎖

void clean(void *arg)

{

printf(“Clean up:%s

”,(char*)arg);

pthread_mutex_unlock(&mutex);//釋放鎖

}

void *WorkCancel(void *arg)

{

pthread_mutex_lock(&mutex);

pthread_cleanup_push(clean,“clean up handler”);//清除函數(shù)的push

struct timespec t = {3,0};//取消點

nanosleep(&t,0);

pthread_cleanup_pop(0);//清除

pthread_mutex_unlock(&mutex);

}

void *WorkWhile(void *arg)

{

sleep(5);

pthread_mutex_lock(&mutex);

printf(“i get the mutex

”);//若能拿到資源,則表示取消清理函數(shù)成功!

pthread_mutex_unlock(&mutex);

return NULL;

}

int main()

{

pthread_t cons,prod;

pthread_mutex_init(&mutex,NULL);//互斥鎖初始化

int ret = pthread_create(&prod,NULL,WorkCancel,(void*)&g_bowl);//該線程拿到鎖,然后掛掉

if(ret != 0)

{

perror(“pthread_create”);

return -1;

}

int ret1 = pthread_create(&cons,NULL,WorkWhile,(void*)&ret);//測試該線程是否可以拿到鎖

if(ret1 != 0)

{

perror(“pthread_create”);

return -1;

}

pthread_cancel(prod);//取消該線程

pthread_join(prod,NULL);//線程等待

pthread_join(cons,NULL);//線程等待

pthread_mutex_destroy(&mutex);//銷毀互斥鎖

while(1)

{

sleep(1);

}

return 0;

}

結(jié)果:只要拿到鎖,就表明線程清理函數(shù)成功了。

9.多線程與fork()

永遠不要在多線程程序里面調(diào)用fork。

Linux的fork函數(shù), 會復制一個進程, 對于多線程程序而言, fork函數(shù)復制的是用fork的那個線程, 而并不復制其他的線程。fork之后其他線程都不見了。Linux存在forkall語義的系統(tǒng)調(diào)用, 無法做到將多線程全部復制。

多線程程序在fork之前, 其他線程可能正持有互斥量處理臨界區(qū)的代碼。fork之后, 其他線程都不見了, 那么互斥量的值可能處于不可用的狀態(tài), 也不會有其他線程來將互斥量解鎖。

10.生產(chǎn)者與消費者模型

10.1生產(chǎn)者與消費者模型的本質(zhì)

本質(zhì)上是一個線程安全的隊列,和兩種角色的線程(生產(chǎn)者和消費者)

存在三種關(guān)系:

1、生產(chǎn)者與生產(chǎn)者互斥

2、消費者與消費者互斥

3、生產(chǎn)者與消費者同步+互斥

10.2為什么需要生產(chǎn)者與消費者模型?

生產(chǎn)者和消費者彼此之間不直接通訊,而通過阻塞隊列來進行通訊,所以生產(chǎn)者生成完數(shù)據(jù)之后不用等待消費者處理,直接扔給阻塞隊列,消費者不找生產(chǎn)者要數(shù)據(jù),而是直接從阻塞隊列中取,阻塞隊列就相當于一個緩沖區(qū),平衡了生產(chǎn)者和消費者的處理能力。這個阻塞隊列就是用來給生產(chǎn)者和消費解耦的。

10.3優(yōu)點

1、解耦

2、支持高并發(fā)

3、支持忙閑不均

10.4實現(xiàn)兩個消費者線程,兩個生產(chǎn)者線程的生產(chǎn)者消費者模型

生產(chǎn)者生成時用的同一個全局變量,故對該全局變量進行了加鎖。

#include 《stdio.h》

#include 《stdlib.h》

#include 《unistd.h》

#include 《pthread.h》

#include 《queue》

#include 《sys/syscall.h》

#define PTHREAD_COUNT 2

int data = 0;//全局變量作為插入數(shù)據(jù)

pthread_mutex_t mutex1;

class ModelOfConProd{

public:

ModelOfConProd()//構(gòu)造

{

_capacity = 10;

pthread_mutex_init(&_mutex,NULL);

pthread_cond_init(&_cons,NULL);

pthread_cond_init(&_prod,NULL);

}

~ModelOfConProd()//析構(gòu)

{

_capacity = 0;

pthread_mutex_destroy(&_mutex);

pthread_cond_destroy(&_cons);

pthread_cond_destroy(&_prod);

}

void Push(int data)//push數(shù)據(jù),生產(chǎn)者線程使用的

{

pthread_mutex_lock(&_mutex);

while((int)_queue.size() 》= _capacity)

{

pthread_cond_wait(&_prod,&_mutex);

}

_queue.push(data);

pthread_mutex_unlock(&_mutex);

pthread_cond_signal(&_cons);

}

void Pop(int& data)//pop數(shù)據(jù),消費者線程使用的

{

pthread_mutex_lock(&_mutex);

while(_queue.empty())

{

pthread_cond_wait(&_cons,&_mutex);

}

data = _queue.front();

_queue.pop();

pthread_mutex_unlock(&_mutex);

pthread_cond_signal(&_prod);

}

private:

int _capacity;//容量大小,限制容量大小

std::queue《int》 _queue;//隊列

pthread_mutex_t _mutex;//互斥鎖

pthread_cond_t _cons;//消費者條件變量

pthread_cond_t _prod;//生產(chǎn)者條件變量

};

void *ConsumerStart(void *arg)//消費者入口函數(shù)

{

ModelOfConProd *cp = (ModelOfConProd *)arg;

while(1)

{

cp-》Push(data);

printf(“i am pid : %d,i push :%d

”,(int)syscall(SYS_gettid),data);

pthread_mutex_lock(&mutex1);//++的時候,給該全局變量加鎖

++data;

pthread_mutex_unlock(&mutex1);

}

}

void *ProductsStart(void *arg)//生產(chǎn)者入口函數(shù)

{

ModelOfConProd *cp = (ModelOfConProd *)arg;

int data = 0;

while(1)

{

cp-》Pop(data);

printf(“i am pid : %d,i pop :%d

”,(int)syscall(SYS_gettid),data);

}

}

int main()

{

ModelOfConProd *cp = new ModelOfConProd;

pthread_mutex_init(&mutex1,NULL);

pthread_t cons[PTHREAD_COUNT],prod[PTHREAD_COUNT];

for(int i = 0;i 《 PTHREAD_COUNT; ++i)

{

int ret = pthread_create(&cons[i],NULL,ConsumerStart,(void*)cp);

if(ret 《 0)

{

perror(“pthread_create”);

return -1;

}

ret = pthread_create(&prod[i],NULL,ProductsStart,(void*)cp);

if(ret 《 0)

{

perror(“pthread_create”);

return -1;

}

}

for(int i = 0;i 《 PTHREAD_COUNT;++i)

{

pthread_join(cons[i],NULL);

pthread_join(prod[i],NULL);

}

pthread_mutex_destroy(&mutex1);

return 0;

}

11.寫多線程時應注意

先考慮代碼的核心邏輯(先實現(xiàn))

考慮核心邏輯中是否訪問臨界資源或者說執(zhí)行臨界區(qū)代碼,如果有就需要保持互斥

考慮線程之間是否需要同步

編輯:jq

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學習之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 寄存器
    +關(guān)注

    關(guān)注

    31

    文章

    5355

    瀏覽量

    120531
  • cpu
    cpu
    +關(guān)注

    關(guān)注

    68

    文章

    10873

    瀏覽量

    212020
  • 代碼
    +關(guān)注

    關(guān)注

    30

    文章

    4791

    瀏覽量

    68694

原文標題:多線程詳解,一篇文章徹底搞懂多線程中各個難點

文章出處:【微信號:mcu168,微信公眾號:硬件攻城獅】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    socket 多線程編程實現(xiàn)方法

    是指在同個進程運行多個線程,每個線程可以獨立執(zhí)行任務。線程共享進程的資源,如內(nèi)存空間和文件句柄,但每個
    的頭像 發(fā)表于 11-12 14:16 ?380次閱讀

    智慧公交是什么?帶你詳解智慧公交的解決方案!

    智慧公交是什么?帶你詳解智慧公交的解決方案!
    的頭像 發(fā)表于 11-05 12:26 ?303次閱讀
    智慧公交是什么?<b class='flag-5'>一</b><b class='flag-5'>文</b><b class='flag-5'>帶你</b>詳解智慧公交的解決方案!

    搞懂Linux進程的睡眠和喚醒

    優(yōu)先級、文件描述符(記錄當前進程打開的文件)、主要進程標識的進程號和父進程號: 進程號(PID: Process Identity Number):唯的標識個進程,用于區(qū)分系統(tǒng)各個
    發(fā)表于 11-04 15:15

    Python多線程和多進程的區(qū)別

    Python作為種高級編程語言,提供了多種并發(fā)編程的方式,其中多線程與多進程是最常見的兩種方式之。在本文中,我們將探討Python多線程
    的頭像 發(fā)表于 10-23 11:48 ?411次閱讀
    Python<b class='flag-5'>中</b><b class='flag-5'>多線程</b>和多進程的區(qū)別

    掌握Python多線程

    使用線程可以把占據(jù)長時間的程序的任務放到后臺去處理。
    的頭像 發(fā)表于 08-05 15:46 ?871次閱讀

    ESP32會不會有多線程問題,需要加鎖嗎?

    ESP32會不會有多線程問題,需要加鎖嗎
    發(fā)表于 07-19 08:05

    多線程設計模式到對 CompletableFuture 的應用

    最近在開發(fā) 延保服務 頻道頁時,為了提高查詢效率,使用到了多線程技術(shù)。為了對多線程方案設計有更加充分的了解,在業(yè)余時間讀完了《圖解 Java 多線程設計模式》這本書,覺得收獲良多。本篇文章將介紹其中
    的頭像 發(fā)表于 06-26 14:18 ?369次閱讀
    從<b class='flag-5'>多線程</b>設計模式到對 CompletableFuture 的應用

    bootloader開多線程做引導程序,跳app初始化后直接進hardfualt,為什么?

    如標題,想做個遠程升級的項目,bootloader引導區(qū)域和app都是開多線程跑的,就是自己寫了個小的任務調(diào)度器,沒什么功能主要是想讓程序快速的響應,延時不會對其他程序造成堵塞,程序測試
    發(fā)表于 04-18 06:07

    鴻蒙OS開發(fā)實例:【ArkTS類庫多線程CPU密集型任務TaskPool】

    CPU密集型任務是指需要占用系統(tǒng)資源處理大量計算能力的任務,需要長時間運行,這段時間會阻塞線程其它事件的處理,不適宜放在主線程進行。例如圖像處理、視頻編碼、數(shù)據(jù)分析等。 基于多線程并發(fā)機制處理CPU密集型任務可以提高CPU
    的頭像 發(fā)表于 04-01 22:25 ?852次閱讀
    鴻蒙OS開發(fā)實例:【ArkTS類庫<b class='flag-5'>多線程</b>CPU密集型任務TaskPool】

    鴻蒙APP開發(fā):【ArkTS類庫多線程】TaskPool和Worker的對比

    TaskPool(任務池)和Worker的作用是為應用程序提供多線程的運行環(huán)境,用于處理耗時的計算任務或其他密集型任務??梢杂行У乇苊膺@些任務阻塞主線程,從而最大化系統(tǒng)的利用率,降低整體資源消耗,并提高系統(tǒng)的整體性能。
    的頭像 發(fā)表于 03-26 22:09 ?659次閱讀
    鴻蒙APP開發(fā):【ArkTS類庫<b class='flag-5'>多線程</b>】TaskPool和Worker的對比

    鴻蒙原生應用開發(fā)-ArkTS語言基礎類庫多線程TaskPool和Worker的對比(

    TaskPool(任務池)和Worker的作用是為應用程序提供多線程的運行環(huán)境,用于處理耗時的計算任務或其他密集型任務??梢杂行У乇苊膺@些任務阻塞主線程,從而最大化系統(tǒng)的利用率,降低整體資源消耗
    發(fā)表于 03-25 14:11

    java實現(xiàn)多線程的幾種方式

    Java實現(xiàn)多線程的幾種方式 多線程是指程序包含了兩個或以上的線程,每個線程都可以并行執(zhí)行不同的任務或操作。Java
    的頭像 發(fā)表于 03-14 16:55 ?721次閱讀

    python5種線程鎖盤點

    線程安全是多線程或多進程編程個概念,在擁有共享數(shù)據(jù)的多條線程并行執(zhí)行的程序,
    發(fā)表于 03-07 11:08 ?1602次閱讀
    python<b class='flag-5'>中</b>5種<b class='flag-5'>線程</b>鎖盤點

    AT socket可以多線程調(diào)用嗎?

    請問AT socket 可以多線程調(diào)用嗎? 有互鎖機制嗎,還是要自己做互鎖。
    發(fā)表于 03-01 08:22

    linux多線程編程實例

    linux線程
    的頭像 發(fā)表于 02-15 21:16 ?478次閱讀
    linux<b class='flag-5'>多線程</b>編程實例