1.什么是線程?
linux內(nèi)核中是沒有線程這個(gè)概念的,而是輕量級進(jìn)程的概念:LWP。一般我們所說的線程概念是C庫當(dāng)中的概念。
1.1線程是怎樣描述的?
線程實(shí)際上也是一個(gè)task_struct,工作線程拷貝主線程的task_struct,然后共用主線程的mm_struct。線程ID是在用task_struct中pid描述的,而task_struct中tgid是線程組ID,表示線程屬于該線程組,對于主線程而言,其pid和tgid是相同的,我們一般看到的進(jìn)程ID就是tgid。
即:
但是獲取該gettid系統(tǒng)調(diào)用接口并沒有被封裝起來,如果確實(shí)需要獲取線程ID,可使用:
#include 《sys/syscall.h》
int TID = syscall(SYS_gettid);
則對線程組而言,所有的tgid一定是一樣的,所有的pid一定是不一樣的。主線程pid和tgid一樣,工作線程pid和tgid一定不一樣。
1.2如何查看一個(gè)線程的ID
命令:ps -eLf
上述polkitd進(jìn)程是多線程的,進(jìn)程ID為731,進(jìn)程內(nèi)有6個(gè)線程,線程ID為731,764,765,768,781,791。
1.3多線程如何避免調(diào)用棧混亂的問題?
工作線程和主線程共用一個(gè)mm_struct,如果都向棧中壓棧,必然會導(dǎo)致調(diào)用棧出錯(cuò)。
實(shí)際上工作線程壓棧是壓了共享區(qū),該共享區(qū)包含了許多線程獨(dú)有的資源。如圖:
每一個(gè)線程,默認(rèn)在共享區(qū)中占有的空間為8M,可以使用ulimit -s修改。
進(jìn)程是資源分配的基本單位,線程是調(diào)度的基本單位。
1.3.1線程獨(dú)有資源
線程ID
一組寄存器
errno
信號屏蔽字
調(diào)度優(yōu)先級
1.3.2線程共享資源和環(huán)境
文件描述符表
信號的處理方式
當(dāng)前工作目錄
用戶id和組id
1.4為什么要有多線程?
舉個(gè)生活中的例子, 這就好比去銀行辦理業(yè)務(wù)。到達(dá)銀行后, 首先取一個(gè)號碼, 然后坐下來安心等待。這時(shí)候你一定希望, 辦理業(yè)務(wù)的窗口越多越好。如果把整個(gè)營業(yè)大廳當(dāng)成一個(gè)進(jìn)程的話, 那么每一個(gè)窗口就是一個(gè)工作線程。
1.4.1線程帶來的優(yōu)勢
1、線程會共享內(nèi)存地址空間。
2、創(chuàng)建線程花費(fèi)的時(shí)間要少于創(chuàng)建進(jìn)程花費(fèi)的時(shí)間。
3、終止線程花費(fèi)的時(shí)間要少于終止進(jìn)程花費(fèi)的時(shí)間。
4、線程之間上下文切換的開銷, 要小于進(jìn)程之間的上下文切換。
5、線程之間數(shù)據(jù)的共享比進(jìn)程之間的共享要簡單。
6、充分利用多處理器的可并行數(shù)量。(線程會提高運(yùn)行效率,但當(dāng)線程多到一定程度后,可能會導(dǎo)致效率下降,因?yàn)闀芯€程調(diào)度切換。)
1.4.2線程帶來的缺點(diǎn)
健壯性降低:多個(gè)線程之中, 只要有一個(gè)線程不夠健壯存在bug(如訪問了非法地址引發(fā)的段錯(cuò)誤) , 就會導(dǎo)致進(jìn)程內(nèi)的所有線程一起完蛋。
線程模型作為一種并發(fā)的編程模型, 效率并沒有想象的那么高, 會出現(xiàn)復(fù)雜度高、 易出錯(cuò)、 難以測試和定位的問題。
1.5注意
1、并不是只有主線程才能創(chuàng)建線程, 被創(chuàng)建出來的線程同樣可以創(chuàng)建線程。
2、不存在類似于fork函數(shù)那樣的父子關(guān)系, 大家都?xì)w屬于同一個(gè)線程組, 進(jìn)程ID都相等, group_leader都指向主線程, 而且各有各的線程ID。
通過group_leader指針, 每個(gè)線程都能找到主線程。主線程存在一個(gè)鏈表頭,后面創(chuàng)建的每一個(gè)線程都會鏈入到該雙向鏈表中。
3、并非只有主線程才能調(diào)用pthread_join連接其他線程, 同一線程組內(nèi)的任意線程都可以對某線程執(zhí)行pthread_join函數(shù)。
4、并非只有主線程才能調(diào)用pthread_detach函數(shù), 其實(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:線程標(biāo)識符,是一個(gè)出參
2、attr:線程屬性
3、star_routine:函數(shù)指針,保存線程入口函數(shù)的地址
4、arg:給線程入口函數(shù)傳參
返回值:成功返回0,失敗返回error number
詳解:
第一個(gè)參數(shù)是pthread_t類型的指針, 線程創(chuàng)建成功的話,會將分配的線程ID填入該指針指向的地址。線程的后續(xù)操作將使用該值作為線程的唯一標(biāo)識。
第二個(gè)參數(shù)是pthread_attr_t類型, 通過該參數(shù)可以定制線程的屬性, 比如可以指定新建線程棧的大小、 調(diào)度策略等。如果創(chuàng)建線程無特殊的要求, 該值也可以是NULL, 表示采用默認(rèn)屬性。
第三個(gè)參數(shù)是線程需要執(zhí)行的函數(shù)。創(chuàng)建線程, 是為了讓線程執(zhí)行一定的任務(wù)。線程創(chuàng)建成功之后, 該線程就會執(zhí)行start_routine函數(shù), 該函數(shù)之于線程, 就如同main函數(shù)之于主線程。
第四個(gè)參數(shù)是新建線程執(zhí)行的start_routine函數(shù)的入?yún)ⅰ?/p>
2.1傳入?yún)?shù)arg的選擇
不要使用臨時(shí)變量傳參,使用堆上開辟的變量可以。
例:
#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);//不要傳臨時(shí)變量,這里是示范
if(ret != 0)
{
perror(“pthread_create”);
return -1;
}
while(1)
{
printf(“i am main work thread
”);
sleep(1);
}
return 0;
}
2.2線程ID以及進(jìn)程地址空間
線程獲取自身的ID:
#include 《pthread.h》
pthread_t pthread_self(void);
判斷兩個(gè)線程ID是否對應(yīng)著同一個(gè)線程:
#include 《pthread.h》
int pthread_equal(pthread_t t1, pthread_t t2);
返回為0時(shí),則表示兩個(gè)線程為同一個(gè)線程,非0時(shí),表示不是同一個(gè)線程。
用戶調(diào)用pthread_create函數(shù)時(shí), 首先要為線程分配線程棧, 而線程棧的位置就落在共享區(qū)。調(diào)用mmap函數(shù)為線程分配??臻g。pthread_create函數(shù)分配的pthread_t類型的線程ID, 不過是分配出來的空間里的一個(gè)地址, 更確切地說是一個(gè)結(jié)構(gòu)體的指針。
2.3線程注意點(diǎn)
1、線程ID是進(jìn)程地址空間內(nèi)的一個(gè)地址, 要在同一個(gè)線程組內(nèi)進(jìn)行線程之間的比較才有意義。不同線程組內(nèi)的兩個(gè)線程, 哪怕兩者的pthread_t值是一樣的, 也不是同一個(gè)線程。
2、線程ID就有可能會被復(fù)用:
1、線程退出。
2、線程組的其他線程對該線程執(zhí)行了pthread_join, 或者線程退出前將分離狀態(tài)設(shè)置為已分離。
3、再次調(diào)用pthread_create創(chuàng)建線程。
2.4線程創(chuàng)建出來的默認(rèn)值
線程創(chuàng)建的第二個(gè)參數(shù)是pthread_attr_t類型的指針, pthread_attr_init函數(shù)會將線程的屬性重置成默認(rèn)值。
如果確實(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.線程終止
線程終止,但進(jìn)程不會終止的方法:
1、入口函數(shù)的return返回,線程就退出了
2、線程調(diào)用pthread_exit(NULL),誰調(diào)用誰退出
#include 《pthread.h》
void pthread_exit(void *retval);
參數(shù):retval是返回信息,”臨終遺言“,可以給可以不給
該變量不能使用臨時(shí)變量。
可使用:全局變量、堆上開辟的空間、字符串常量。
pthread_exit和線程啟動(dòng)函數(shù)(start_routine) 執(zhí)行return是有區(qū)別的。在start_routine中調(diào)用的任何層級的函數(shù)執(zhí)行pthread_exit() 都會引發(fā)線程退出, 而return, 只能是在start_routine函數(shù)內(nèi)執(zhí)行才能導(dǎo)致線程退出。
3、其它線程調(diào)用了pthread_cancel函數(shù)取消了該線程
int pthread_cancel(pthread_t thread);
thread:線程標(biāo)識符
調(diào)用該函數(shù)的執(zhí)行流可以取消其它線程,但是需要知道其它線程的線程標(biāo)識符,也可以執(zhí)行流自己取消自己,傳入自己的線程標(biāo)識符。
如果線程組中的任何一個(gè)線程調(diào)用了exit函數(shù), 或者主線程在main函數(shù)中執(zhí)行了return語句, 那么整個(gè)線程組內(nèi)的所有線程都會終止。
4.線程等待
4.1線程等待接口
#include 《pthread.h》
int pthread_join(pthread_t thread, void **retval);
調(diào)用該函數(shù),該執(zhí)行流在等待線程退出的時(shí)候,該執(zhí)行流是阻塞在pthread_joind當(dāng)中的。
4.2線程等待和進(jìn)程等待的不同
第一點(diǎn)不同之處是進(jìn)程之間的等待只能是父進(jìn)程等待子進(jìn)程, 而線程則不然。線程組內(nèi)的成員是對等的關(guān)系, 只要是在一個(gè)線程組內(nèi), 就可以對另外一個(gè)線程執(zhí)行連接(join) 操作。
第二點(diǎn)不同之處是進(jìn)程可以等待任一子進(jìn)程的退出 , 但是線程的連接操作沒有類似的接口, 即不能連接線程組內(nèi)的任一線程, 必須明確指明要連接的線程的線程ID。
4.3為什么要等待退出的線程?
如果不連接已經(jīng)退出的線程, 會導(dǎo)致資源無法釋放。所謂資源指的又是什么呢?
1、已經(jīng)退出的線程, 其空間沒有被釋放, 仍然在進(jìn)程的地址空間之內(nèi)。
2、新創(chuàng)建的線程, 沒有復(fù)用剛才退出的線程的地址空間。
如果不執(zhí)行連接操作, 線程的資源就不能被釋放, 也不能被復(fù)用, 這就造成了資源的泄漏。
縱然調(diào)用了pthread_join, 也并沒有立即調(diào)用munmap來釋放掉退出線程的棧, 它們是被后建的線程復(fù)用了。釋放線程資源的時(shí)候, 若進(jìn)程可能再次創(chuàng)建線程, 而頻繁地munmap和mmap會影響性能, 所以將該棧緩存起來, 放到一個(gè)鏈表之中, 如果有新的創(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);//不要傳臨時(shí)變量,這里是示范
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);
默認(rèn)情況下, 新創(chuàng)建的線程處于可連接(Joinable) 的狀態(tài), 可連接狀態(tài)的線程退出后, 需要對其執(zhí)行連接操作, 否則線程資源無法釋放, 從而造成資源泄漏。
如果其他線程并不關(guān)心線程的返回值, 那么連接操作就會變成一種負(fù)擔(dān):你不需要它, 但是你不去執(zhí)行連接操作又會造成資源泄漏。這時(shí)候你需要的東西只是:線程退出時(shí), 系統(tǒng)自動(dòng)將線程相關(guān)的資源釋放掉, 無須等待連接。
可以是線程組內(nèi)其他線程對目標(biāo)線程進(jìn)行分離, 也可以是線程自己執(zhí)行pthread_detach函數(shù)。
線程的狀態(tài)之中, 可連接狀態(tài)和已分離狀態(tài)是沖突的, 一個(gè)線程不能既是可連接的, 又是已分離的。因此, 如果線程處于已分離的狀態(tài), 其他線程嘗試連接線程時(shí), 會返回EINVAL錯(cuò)誤。
注意:這里的已分離不是指線程失去控制,不歸線程組管,而是指線程退出后,系統(tǒng)會自動(dòng)釋放線程資源。若是線程組內(nèi)的任意線程執(zhí)行了exit函數(shù),即使是已分離的線程,也仍會收到影響,一并退出。
6.線程安全
線程安全中涉及到的概念:
臨界資源:多線程中都能訪問到的資源
臨界區(qū):每個(gè)線程內(nèi)部,訪問臨界資源的代碼,就叫臨界區(qū)
6.1什么是線程不安全?
多個(gè)線程訪問同一塊臨界資源,導(dǎo)致資源產(chǎn)生二義性的現(xiàn)象。
6.1.1舉一個(gè)例子
假設(shè)現(xiàn)在有兩個(gè)線程A和B,單核CPU的情況下,此時(shí)有一個(gè)int類型的全局變量為100,A和B的入口函數(shù)都要對這個(gè)全局變量進(jìn)行–操作。
線程A先拿到CPU資源后,對全局變量進(jìn)行–操作并不是原子性操作,也就是意味著,A在執(zhí)行–的過程中有可能會被打斷。假設(shè)A剛剛將全局變量的值讀到寄存器當(dāng)中,就被切換出去了,此時(shí)程序計(jì)數(shù)器保存了下一條執(zhí)行的指令,上下文信息保存寄存器中的值,這兩個(gè)東西是用來線程A再次拿到CPU資源后,恢復(fù)現(xiàn)場使用的。
此時(shí),線程B拿到了CPU資源,對全局變量進(jìn)行了–操作,并且將100減為了99,回寫到了內(nèi)存中。
A再次擁有了CPU資源后,恢復(fù)現(xiàn)場,繼續(xù)往下執(zhí)行,從寄存器中讀到的值仍為100,減完之后為99,回寫到內(nèi)存中為99。
上述例子中,線程A和B都對全局變量進(jìn)行了–操作,全局變量的值應(yīng)該變?yōu)?8,但程序現(xiàn)在實(shí)際的結(jié)果為99,所以這就導(dǎo)致了線程不安全。
6.2如何解決線程不安全現(xiàn)象?
解決方案只需做到下述三點(diǎn)即可:
1、代碼必須要有互斥的行為:當(dāng)一個(gè)線程正在臨界區(qū)中執(zhí)行時(shí), 不允許其他線程進(jìn)入該臨界區(qū)中。
2、如果多個(gè)線程同時(shí)要求執(zhí)行臨界區(qū)的代碼, 并且當(dāng)前臨界區(qū)并沒有線程在執(zhí)行, 那么只能允許一個(gè)線程進(jìn)入該臨界區(qū)。
3、如果線程不在臨界區(qū)中執(zhí)行, 那么該線程不能阻止其他線程進(jìn)入臨界區(qū)。
鎖是一個(gè)很普遍的需求, 當(dāng)然用戶可以自行實(shí)現(xiàn)鎖來保護(hù)臨界區(qū)。但是實(shí)現(xiàn)一個(gè)正確并且高效的鎖非常困難??v然拋下高效不談, 讓用戶從零開始實(shí)現(xiàn)一個(gè)正確的鎖也并不容易。正是因?yàn)檫@種需求具有普遍性, 所以Linux提供了互斥量。
6.3互斥量接口
6.3.1互斥量的初始化
1、靜態(tài)分配:
#include 《pthread.h》
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
2、動(dòng)態(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、不要銷毀一個(gè)已加鎖的互斥量, 或者是真正配合條件變量使用的互斥量。
3、已經(jīng)銷毀的互斥量, 要確保后面不會有線程再嘗試加鎖。
當(dāng)互斥量處于已加鎖的狀態(tài), 或者正在和條件變量配合使用, 調(diào)用pthread_mutex_destroy函數(shù)會返回EBUSY錯(cuò)誤碼。
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);
第一個(gè)接口:int pthread_mutex_lock(pthread_mutex_t *mutex);
1、該接口是阻塞加鎖接口。
2、mutex為傳入互斥鎖變量的地址
3、如果mutex當(dāng)中的計(jì)數(shù)器為1,pthread_mutex_lock接口就返回了,表示加鎖成功,同時(shí)計(jì)數(shù)器當(dāng)中的值會被更改為0.
4、如果mutex當(dāng)中的計(jì)數(shù)器為0,pthread_mutex_lock接口就阻塞了,pthread_mutex_lock接口沒有返回了,阻塞在函數(shù)內(nèi)部,直到加鎖成功
第二個(gè)接口:int pthread_mutex_trylock(pthread_mutex_t *mutex);
1、該接口為非阻塞接口
2、mutex中計(jì)數(shù)器為1時(shí),加鎖成功,計(jì)數(shù)器置為0,然后返回
3、mutex中計(jì)數(shù)器為0時(shí),加鎖失敗,但也會返回,此時(shí)加鎖是失敗狀態(tài),一定不要去訪問臨界資源
4、非阻塞接口一般都需要搭配循環(huán)來使用。
第三個(gè)接口:int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout);
1、帶有超時(shí)時(shí)間的加鎖接口
2、不能直接獲取互斥鎖的時(shí)候,會等待abs_timeout時(shí)間
3、如果在這個(gè)時(shí)間內(nèi)加鎖成功了,直接返回,不需要再繼續(xù)等待剩余的時(shí)間,并且表示加鎖成功
4、如果超出了該時(shí)間,也返回了,但是加鎖失敗了,需要循環(huán)加鎖
上述三個(gè)加鎖接口,第一個(gè)接口用的最多。
6.3.4互斥量的解鎖
int pthread_mutex_unlock(pthread_mutex_t *mutex);
對上述所有的加鎖接口,都可使用該函數(shù)解鎖
解鎖的時(shí)候,會將互斥鎖當(dāng)中計(jì)數(shù)器的值從0變?yōu)?,表示其它線程可以獲取互斥量
6.4互斥鎖的本質(zhì)
1、在互斥鎖內(nèi)部有一個(gè)計(jì)數(shù)器,其實(shí)就是互斥量,計(jì)數(shù)器的值只能為0或者為1
2、當(dāng)線程獲取互斥鎖的時(shí)候,如果計(jì)數(shù)器當(dāng)前值為0,表示當(dāng)前線程不能獲取到互斥鎖,也就是沒有獲取到互斥鎖,就不要去訪問臨界資源
3、當(dāng)前線程獲取互斥鎖的時(shí)候,如果計(jì)數(shù)器當(dāng)前值為1,表示當(dāng)前線程可以獲取到互斥鎖,也就是意味著可以訪問臨界資源
6.5互斥鎖中的計(jì)數(shù)器如何保證了原子性?
獲取鎖資源的時(shí)候(加鎖):
1、寄存器當(dāng)中值直接賦值為0
2、將寄存器當(dāng)中的值和計(jì)數(shù)器當(dāng)中的值進(jìn)行交換
3、判斷寄存器當(dāng)中的值,得出加鎖結(jié)果
例:4個(gè)線程,對同一個(gè)全局變量進(jìn)行減減操作
#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);//在所有可能退出的地方,進(jìn)行解鎖
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);//不要傳臨時(shí)變量,這里是示范
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)核維護(hù)等待隊(duì)列, 互斥量實(shí)現(xiàn)了大體上的公平;由于等待線程被喚醒后, 并不自動(dòng)持有互斥量, 需要和剛進(jìn)入臨界區(qū)的線程競爭(搶鎖), 所以互斥量并沒有做到先來先服務(wù)。
6.7互斥鎖的類型
1、PTHREAD_MUTEX_NORMAL:最普通的一種互斥鎖。它不具備死鎖檢測功能, 如線程對自己鎖定的互斥量再次加鎖, 則會發(fā)生死鎖。
2、
PTHREAD_MUTEX_RECURSIVE_NP:支持遞歸的一種互斥鎖, 該互斥量的內(nèi)部維護(hù)有互斥鎖的所有者和一個(gè)鎖計(jì)數(shù)器。當(dāng)線程第一次取到互斥鎖時(shí), 會將鎖計(jì)數(shù)器置1, 后續(xù)同一個(gè)線程再次執(zhí)行加鎖操作時(shí), 會遞增該鎖計(jì)數(shù)器的值。解鎖則遞減該鎖計(jì)數(shù)器的值, 直到降至0, 才會真正釋放該互斥量, 此時(shí)其他線程才能獲取到該互斥量。解鎖時(shí), 如果互斥量的所有者不是調(diào)用解鎖的線程, 則會返回EPERM。
3、
PTHREAD_MUTEX_ERRORCHECK_NP:支持死鎖檢測的互斥鎖?;コ饬康膬?nèi)部會記錄互斥鎖的當(dāng)前所有者的線程ID(調(diào)度域的線程ID) 。如果互斥量的持有線程再次調(diào)用加鎖操作, 則會返回EDEADLK。解鎖時(shí), 如果發(fā)現(xiàn)調(diào)用解鎖操作的線程并不是互斥鎖的持有者, 則會返回EPERM。
4、自旋鎖,自旋鎖采用了和互斥量完全不同的策略, 自旋鎖加鎖失敗, 并不會讓出CPU, 而是不停地嘗試加鎖, 直到成功為止。這種機(jī)制在臨界區(qū)非常小且對臨界區(qū)的爭奪并不激烈的場景下, 效果非常好。自旋鎖的效果好, 但是副作用也大, 如果使用不當(dāng), 自旋鎖的持有者遲遲無法釋放鎖, 那么, 自旋接近于死循環(huán), 會消耗大量的CPU資源, 造成CPU使用率飆高。因此, 使用自旋鎖時(shí), 一定要確保臨界區(qū)盡可能地小, 不要有系統(tǒng)調(diào)用, 不要調(diào)用sleep。使用strcpy/memcpy等函數(shù)也需要謹(jǐn)慎判斷操作內(nèi)存的大小, 以及是否會引起缺頁中斷。
5、PTHREAD_MUTEX_ADAPTIVE_NP:自適應(yīng)鎖,首先與自旋鎖一樣, 持續(xù)嘗試獲取, 但過了一定時(shí)間仍然不能申請到鎖, 就放棄嘗試, 讓出CPU并等待。PTHREAD_MUTEX_ADAPTIVE_NP類型的互斥量, 采用的就是這種機(jī)制。
6.8死鎖和活鎖
線程1已經(jīng)成功拿到了互斥量1, 正在申請互斥量2, 而同時(shí)在另一個(gè)CPU上,線程2已經(jīng)拿到了互斥量2, 正在申請互斥量1。彼此占有對方正在申請的互斥量,結(jié)局就是誰也沒辦法拿到想要的互斥量, 于是死鎖就發(fā)生了。
6.8.1死鎖概念
死鎖是指在一組進(jìn)程中的各個(gè)進(jìn)程均占有不會釋放的資源,但因互相申請被其它進(jìn)程所占有不會釋放的資源而處于一種永久等待的狀態(tài)。
6.8.2死鎖的四個(gè)必要條件
1、互斥條件:一個(gè)資源只能被一個(gè)執(zhí)行流使用
2、請求與保持條件:一個(gè)執(zhí)行流因請求資源而阻塞時(shí),對已獲得的資源不會釋放
3、不剝奪條件:一個(gè)執(zhí)行流已獲得的資源,在未使用完之前,不能強(qiáng)行剝奪
4、循環(huán)等待條件:若干執(zhí)行流之間形成一種頭尾相接的循環(huán)等待資源的關(guān)系
6.8.3避免死鎖
1、破壞死鎖的四個(gè)必要條件(實(shí)際上只能破壞條件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);
這兩個(gè)函數(shù)反映了一種,不行就算了的思想。
trylock不行就回退的思想有可能會引發(fā)活鎖(live lock) 。生活中也經(jīng)常遇到兩個(gè)人迎面走來, 雙方都想給對方讓路, 但是讓的方向卻不協(xié)調(diào), 反而互相堵住的情況 。活鎖現(xiàn)象與這種場景有點(diǎn)類似。
線程1首先申請鎖mutex_a后, 之后嘗試申請mutex_b, 失敗以后, 釋放mutex_a進(jìn)入下一輪循環(huán), 同時(shí)線程2會因?yàn)閲L試申請mutex_a失敗,而釋放mutex_b, 如果兩個(gè)線程恰好一直保持這種節(jié)奏, 就可能在很長的時(shí)間內(nèi)兩者都一次次地擦肩而過。當(dāng)然這畢竟不是死鎖, 終究會有一個(gè)線程同時(shí)持有兩把鎖而結(jié)束這種情況。盡管如此, 活鎖的確會降低性能。
6.8.5死鎖調(diào)試
查看多個(gè)線程堆棧: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、找到進(jìn)程號
2、開始調(diào)試
3、查看多個(gè)線程堆棧
4、跳轉(zhuǎn)到線程中
5、查看具體調(diào)用堆棧
6、查看互斥鎖1和互斥鎖2,分別被誰拿著
6.9讀寫鎖
6.9.1什么是讀寫鎖?
大部分情況下,對于共享變量的訪問特點(diǎn):只是讀取共享變量的值,而不是修改,只有在少數(shù)情況下,才會真正的修改共享變量的值。
在這種情況下,讀請求之間是同步的,它們之間的并發(fā)訪問是安全的。然而寫請求必須鎖住讀請求和其它寫請求。
即讀線程可多個(gè)同時(shí)讀,而寫線程只允許同一時(shí)間內(nèi)一個(gè)線程去寫。
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初始化的讀寫鎖,在不需要讀寫鎖的時(shí)候,需要調(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); //非阻塞類型的讀加鎖接口
最大的好處就是,允許多個(gè)線程以只讀加鎖的方式獲取到讀寫鎖;
本質(zhì)上,讀寫鎖的內(nèi)部維護(hù)了一個(gè)引用計(jì)數(shù),每當(dāng)線程以讀方式獲取讀寫鎖時(shí),該引用計(jì)數(shù)+1;
當(dāng)釋放以讀加鎖的方式的讀寫鎖時(shí),會先對引用計(jì)數(shù)進(jìn)行-1,直到引用計(jì)數(shù)的值為0的時(shí)候,才真正釋放了這把讀寫鎖。
6.9.4寫者加鎖
#include 《pthread.h》
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);// 非阻塞寫
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);//阻塞寫
寫鎖用的是獨(dú)占模式,如果當(dāng)前讀寫鎖被某寫線程占用著,則不允許任何讀鎖通過請求,也不允許任何寫鎖請求通過,讀鎖請求和寫鎖請求都要陷入阻塞,直到線程釋放寫鎖。
6.9.5 解鎖
#include 《pthread.h》
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
不論是讀者加鎖還是寫者加鎖,都采用該接口進(jìn)行解釋。
讀者解鎖,只有當(dāng)引用計(jì)數(shù)為0的時(shí)候,才真正釋放了讀寫鎖。
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)先:讀鎖來請求可以立即響應(yīng),只要有一個(gè)讀鎖沒完成,那么寫鎖就無法寫。這種策略是不公平的,極端情況下,寫現(xiàn)場很可能被餓死,即線程總是拿不到鎖資源。
寫者優(yōu)先:只要線程申請了寫鎖,那么在寫鎖后面到來的讀鎖請求就會統(tǒng)統(tǒng)被阻塞,不能先于寫鎖拿到鎖。
讀寫鎖實(shí)現(xiàn)中的變量及含義
對于讀請求而言:如果
1. 無線程持有寫鎖,即_writer = 0.
2. 采用讀者優(yōu)先策略或者當(dāng)前沒有寫鎖申請請求,即 _nr_writers_queue = 0
3. 當(dāng)滿足這兩個(gè)條件時(shí),讀鎖請求立即獲得讀鎖,返回之前執(zhí)行_nr_readers++,表示多了一個(gè)線程正在讀
4. 不滿足這兩個(gè)條件時(shí),執(zhí)行_nr_readers_queued++,表示增加了一個(gè)讀鎖等待者,然后調(diào)用futex,陷入阻塞。醒來之后,執(zhí)行_nr_readers_queued- -,再次判斷是否滿足條件1,2
對于寫請求而言:如果
1. 無線程持有寫鎖,即_writer = 0.
2. 沒有線程持有讀鎖,即_nr_readers = 0.
3. 如果上述條件滿足,就會立即拿到鎖,將_writer 置為當(dāng)前線程的ID
4. 如果不滿足,則執(zhí)行_nr_writers_queue++, 表示增加了一個(gè)寫鎖等待者線程,然后執(zhí)行futex陷入等待。醒來后,先執(zhí)行_nr_writers_queue- -,再繼續(xù)判斷條件1,2
對于解鎖,如果當(dāng)前是寫鎖:
1. 執(zhí)行_writer = 0.,表示釋放寫鎖。
2. 根據(jù)_nr_writers_queue判斷有沒有寫鎖,如果有則喚醒一個(gè)寫鎖,如果沒有寫鎖等待者,則喚醒所有的讀鎖等待者。
對于解鎖,如果當(dāng)前是讀鎖:
1. 執(zhí)行_nr_readers- -,表示讀鎖占有者少了一個(gè)。
2. 判斷_nr_readers是否等于0,是的話則表示當(dāng)前線程是最后一個(gè)讀鎖占有者,需要喚醒寫鎖等待者或讀鎖等待者
3. 根據(jù)_nr_writers_queue判斷是否存在寫鎖等待者,若有,則喚醒一個(gè)寫鎖等待線程
4. 如果沒有寫鎖等待者,判斷是否存在讀鎖等待者,若有,則喚醒全部的讀鎖等待者
讀寫鎖很容易造成,讀者餓死或者寫者餓死。
也可以設(shè)計(jì)公平的讀寫鎖。
代碼:
#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)車間都停工了, 工人們都在車間睡覺。突然進(jìn)來一批原料, 如果原料充足, 你會發(fā)廣播給所有車間, 原料來了, 快來開工吧。如果進(jìn)來的原料很少, 只夠一個(gè)車間開工的, 你可能只會通知一個(gè)車間開工。
7.2如何做到線程間同步?
條件等待是線程間同步的另一種方法。
如果條件不滿足, 它能做的事情就是等待, 等到條件滿足為止。通常條件的達(dá)成, 很可能取決于另一個(gè)線程, 比如生產(chǎn)者-消費(fèi)者模型。當(dāng)另外一個(gè)線程發(fā)現(xiàn)條件符合的時(shí)候, 它會選擇一個(gè)時(shí)機(jī)去通知等待在這個(gè)條件上的線程。有兩種可能性, 一種是喚醒一個(gè)線程, 一種是廣播, 喚醒其他線程。
則在這個(gè)情況下,需要做到:
1、線程在條件不滿足的情況下, 主動(dòng)讓出互斥量, 讓其他線程去折騰, 線程在此處等待, 等待條件的滿足;
2、一旦條件滿足, 線程就可以立刻被喚醒。
3、線程之所以可以安心等待, 依賴的是其他線程的協(xié)作, 它確信會有一個(gè)線程在發(fā)現(xiàn)條件滿足以后, 將向它發(fā)送信號, 并且讓出互斥量。
7.3條件變量
本質(zhì)上是PCB等待隊(duì)列 + 等待接口 + 喚醒接口。
7.3.1條件變量的初始化
靜態(tài)初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
動(dòng)態(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);
為什么這兩個(gè)接口中有互斥鎖?
條件不會無緣無故地突然變得滿足了, 必然會牽扯到共享數(shù)據(jù)的變化。所以一定要有互斥鎖來保護(hù)。沒有互斥鎖, 就無法安全地獲取和修改共享數(shù)據(jù)。
同步并沒有保證互斥,而保證互斥是使用到了互斥鎖。
pthread_mutex_lock(&m)
while(condition_is_false)
{
pthread_mutex_unlock(&m);
//解鎖之后, 等待之前, 可能條件已經(jīng)滿足, 信號已經(jīng)發(fā)出, 但是該信號可能會被錯(cuò)過
cond_wait(&cv);
pthread_mutex_lock(&m);
}
上面的解鎖和等待不是原子操作。解鎖以后, 調(diào)用cond_wait之前,如果已經(jīng)有其他線程獲取到了互斥量, 并且滿足了條件, 同時(shí)發(fā)出了通知信號, 那么cond_wait將錯(cuò)過這個(gè)信號, 可能會導(dǎo)致線程永遠(yuǎn)處于阻塞狀態(tài)。所以解鎖加等待必須是一個(gè)原子性的操作, 以確保已經(jīng)注冊到事件的等待隊(duì)列之前, 不會有其他線程可以獲得互斥量。
那先注冊等待事件, 后釋放鎖不行嗎?注意, 條件等待是個(gè)阻塞型的接口, 不單單是注冊在事件的等待隊(duì)列上, 線程也會因此阻塞于此, 從而導(dǎo)致互斥量無法釋放, 其他線程獲取不到互斥量, 也就無法通過改變共享數(shù)據(jù)使等待的條件得到滿足, 因此這就造成了死鎖。
pthread_mutex_lock(&m);
while(condition_is_false)
pthread_cond_wait(&v,&m);//此處會阻塞
/*如果代碼運(yùn)行到此處, 則表示我們等待的條件已經(jīng)滿足了,
*并且在此持有了互斥量
*/
/*在滿足條件的情況下, 做你想做的事情。
*/
pthread_mutex_unlock(&m);
pthread_cond_wait函數(shù)只能由擁有互斥量的線程來調(diào)用, 當(dāng)該函數(shù)返回的時(shí)候, 系統(tǒng)會確保該線程再次持有互斥量, 所以這個(gè)接口容易給人一種誤解, 就是該線程一直在持有互斥量。事實(shí)上并不是這樣的。這個(gè)接口向系統(tǒng)聲明了我在PCB等待序列中之后, 就把互斥量給釋放了。這樣其他線程就有機(jī)會持有互斥量,操作共享數(shù)據(jù), 觸發(fā)變化, 使線程等待的條件得到滿足。
pthread_cond_wait內(nèi)部會進(jìn)行解鎖邏輯,則一定要先放到PCB等待序列中,再進(jìn)行解鎖。
while(condition_is_false)
pthread_cond_wait(&v,&m);//此處會阻塞
if(condition_is_false)
pthread_cond_wait(&v,&m);//此處會阻塞
喚醒以后, 再次檢查條件是否滿足, 是不是多此一舉?
因?yàn)閱拘阎写嬖谔摷賳拘眩╯purious wakeup) , 換言之,條件尚未滿足, pthread_cond_wait就返了。在一些實(shí)現(xiàn)中, 即使沒有其他線程向條件變量發(fā)送信號, 等待此條件變量的線程也有可能會醒來。
條件滿足了發(fā)送信號, 但等到調(diào)用pthread_cond_wait的線程得到CPU資源時(shí), 條件又再次不滿足了。好在無論是哪種情況, 醒來之后再次測試條件是否滿足就可以解決虛假等待的問題。
pthread_cond_wait內(nèi)部實(shí)現(xiàn)邏輯:
將調(diào)用pthread_cond_wait函數(shù)的執(zhí)行流放入到PCB等待隊(duì)列當(dāng)中
解鎖
等待被喚醒
被喚醒之后:
1、從PCB等待隊(duì)列中移除出來
2、搶占互斥鎖
情況1:拿到互斥鎖,pthread_cond_wait就返回了
情況2:沒有拿到互斥鎖,阻塞在pthread_cond_wait內(nèi)部搶鎖的邏輯中
當(dāng)阻塞在pthread_cond_wait函數(shù)搶鎖邏輯中時(shí),一旦執(zhí)行流時(shí)間耗盡,意味著線程就被切換出來了,程序計(jì)數(shù)器就保存的是搶鎖的指令,上下文信息保存的就是寄存器的值
當(dāng)再次擁有CPU資源后,恢復(fù)搶鎖邏輯
直到搶鎖成功,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負(fù)責(zé)喚醒等待在條件變量上的一個(gè)線程。
pthread_cond_broadcast,就是廣播喚醒等待在條件變量上的所有線程。
先發(fā)送信號,然后解鎖互斥量,這個(gè)順序是必須的嘛?
先通知條件變量、 后解鎖互斥量, 效率會比先解鎖、 后通知條件變量低。因?yàn)橄韧ㄖ蠼怄i, 執(zhí)行pthread_cond_wait的線程可能在互斥量已然處于加鎖狀態(tài)的時(shí)候醒來, 發(fā)現(xiàn)互斥量仍然沒有解鎖, 就會再次休眠, 從而導(dǎo)致了多余的上下文切換。
7.3.4條件變量的銷毀
int pthread_cond_destroy(pthread_cond_t *cond);
注意:
1、永遠(yuǎn)不要用一個(gè)條件變量對另一個(gè)條件變量賦值, 即pthread_cond_t cond_b = cond_a不合法, 這種行為是未定義的。
2、使用PTHREAD_COND_INITIALIZE靜態(tài)初始化的條件變量, 不需要被銷毀。
3、要調(diào)用pthread_cond_destroy銷毀的條件變量可以調(diào)用pthread_cond_init重新進(jìn)行初始化。
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);//通知消費(fèi)者
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;
}
在這里為什么有兩個(gè)條件變量呢?
若所有的線程只使用一個(gè)條件變量,會導(dǎo)致所有線程最后都進(jìn)入PCB等待隊(duì)列。
thread apply all bt查看:
7.3.5情況分析:兩個(gè)生產(chǎn)者,兩個(gè)消費(fèi)者,一個(gè)PCB等待隊(duì)列
1、最開始的情況,兩個(gè)消費(fèi)者搶到了鎖,此時(shí)生產(chǎn)者未生產(chǎn),則都放入PCB等待隊(duì)列中
2、一個(gè)生產(chǎn)者搶到了鎖,生產(chǎn)了一份材料,喚醒一個(gè)消費(fèi)者,此時(shí)三者搶鎖,若兩個(gè)生產(chǎn)者分別先后搶到了鎖,則都進(jìn)入PCB等待隊(duì)列中
3、只有一個(gè)消費(fèi)者,則必會搶到鎖,消費(fèi)材料,喚醒PCB等待隊(duì)列,若此時(shí)喚醒的是,消費(fèi)者,則現(xiàn)在是這樣一個(gè)情況:
4、兩個(gè)消費(fèi)者在外邊搶鎖,一定都會進(jìn)入PCB等待隊(duì)列中
解決上述問題可采用兩種方法:
1、使用int pthread_cond_broadcast(pthread_cond_t *cond);,喚醒PCB等待隊(duì)列中所有的線程。此時(shí)所有線程都會同時(shí)執(zhí)行搶鎖邏輯,太消費(fèi)資源了。此方法不妥
2、采用兩個(gè)PCB等待序列,一個(gè)放生產(chǎn)者,一個(gè)放消費(fèi)者,生產(chǎn)者喚醒消費(fèi)者,消費(fèi)者喚醒生產(chǎn)者。
8.線程取消
8.1線程取消函數(shù)接口
int pthread_cancel(pthread_t thread);
一個(gè)線程可以通過調(diào)用該函數(shù)向另一個(gè)線程發(fā)送取消請求。這不是個(gè)阻塞型接口, 發(fā)出請求后, 函數(shù)就立刻返回了, 而不會等待目標(biāo)線程退出之后才返回。
調(diào)用pthread_cancel時(shí), 會向目標(biāo)線程發(fā)送一個(gè)SIGCANCEL的信號, 該信號就是kill -l中消失的32號信號。
線程的默認(rèn)取消狀態(tài)是PTHREAD_CANCEL_ENABLE。即是可被取消的。
什么是取消點(diǎn)?可通過man pthreads查看取消點(diǎn)
就是對于某些函數(shù), 如果線程允許取消且取消類型是延遲取消, 并且線程也收到了取消請求, 那么當(dāng)執(zhí)行到這些函數(shù)的時(shí)候, 線程就可以退出了。
8.2線程取消帶來的弊端
目標(biāo)線程可能會持有互斥量、 信號量或其他類型的鎖, 這時(shí)候如果收到取消請求, 并且取消類型是異步取消, 那么可能目標(biāo)線程掌握的資源還沒有來得及釋放就被迫退出了, 這可能會給其他線程帶來不可恢復(fù)的后果, 比如死鎖(其他線程再也無法獲得資源) 。
注意:
輕易不要調(diào)用pthread_cancel函數(shù), 在外部殺死線程是很糟糕的做法,畢竟如果想通知目標(biāo)線程退出, 還可以采取其他方法。
如果不得不允許線程取消, 那么在某些非常關(guān)鍵不容有失的代碼區(qū)域, 暫時(shí)將線程設(shè)置成不可取消狀態(tài), 退出關(guān)鍵區(qū)域之后, 再恢復(fù)成可以取消的狀態(tài)。
在非關(guān)鍵的區(qū)域, 也要將線程設(shè)置成延遲取消, 永遠(yuǎn)不要設(shè)置成異步取消。
8.2線程清理函數(shù)
假設(shè)遇到取消請求, 線程執(zhí)行到了取消點(diǎn), 卻沒有來得及做清理動(dòng)作(如動(dòng)態(tài)申請的內(nèi)存沒有釋放, 申請的互斥量沒有解鎖等) , 可能會導(dǎo)致錯(cuò)誤的產(chǎn)生, 比如死鎖, 甚至是進(jìn)程崩潰。
為了避免這種情況, 線程可以設(shè)置一個(gè)或多個(gè)清理函數(shù), 線程取消或退出時(shí),會自動(dòng)執(zhí)行這些清理函數(shù), 以確保資源處于一致的狀態(tài)。
如果線程被取消, 清理函數(shù)則會負(fù)責(zé)解鎖操作。
void pthread_cleanup_push(void (*routine)(void *),void *arg);
void pthread_cleanup_pop(int execute);
這兩個(gè)函數(shù)必須同時(shí)出現(xiàn), 并且屬于同一個(gè)語法塊。
何時(shí)會觸發(fā)注冊的清理函數(shù):?
1、當(dāng)線程的主函數(shù)是調(diào)用pthread_exit返回的, 清理函數(shù)總是會被執(zhí)行。
2、當(dāng)線程是被其他線程調(diào)用pthread_cancel取消的, 清理函數(shù)總是會被執(zhí)行。
3、當(dāng)線程的主函數(shù)是通過return返回的, 并且pthread_cleanup_pop的唯一參數(shù)execute是0時(shí), 清理函數(shù)不會被執(zhí)行。
4、線程的主函數(shù)是通過return返回的, 并且pthread_cleanup_pop的唯一參數(shù)execute是非零值時(shí), 清理函數(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};//取消點(diǎn)
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()
永遠(yuǎn)不要在多線程程序里面調(diào)用fork。
Linux的fork函數(shù), 會復(fù)制一個(gè)進(jìn)程, 對于多線程程序而言, fork函數(shù)復(fù)制的是用fork的那個(gè)線程, 而并不復(fù)制其他的線程。fork之后其他線程都不見了。Linux存在forkall語義的系統(tǒng)調(diào)用, 無法做到將多線程全部復(fù)制。
多線程程序在fork之前, 其他線程可能正持有互斥量處理臨界區(qū)的代碼。fork之后, 其他線程都不見了, 那么互斥量的值可能處于不可用的狀態(tài), 也不會有其他線程來將互斥量解鎖。
10.生產(chǎn)者與消費(fèi)者模型
10.1生產(chǎn)者與消費(fèi)者模型的本質(zhì)
本質(zhì)上是一個(gè)線程安全的隊(duì)列,和兩種角色的線程(生產(chǎn)者和消費(fèi)者)
存在三種關(guān)系:
1、生產(chǎn)者與生產(chǎn)者互斥
2、消費(fèi)者與消費(fèi)者互斥
3、生產(chǎn)者與消費(fèi)者同步+互斥
10.2為什么需要生產(chǎn)者與消費(fèi)者模型?
生產(chǎn)者和消費(fèi)者彼此之間不直接通訊,而通過阻塞隊(duì)列來進(jìn)行通訊,所以生產(chǎn)者生成完數(shù)據(jù)之后不用等待消費(fèi)者處理,直接扔給阻塞隊(duì)列,消費(fèi)者不找生產(chǎn)者要數(shù)據(jù),而是直接從阻塞隊(duì)列中取,阻塞隊(duì)列就相當(dāng)于一個(gè)緩沖區(qū),平衡了生產(chǎn)者和消費(fèi)者的處理能力。這個(gè)阻塞隊(duì)列就是用來給生產(chǎn)者和消費(fèi)解耦的。
10.3優(yōu)點(diǎn)
1、解耦
2、支持高并發(fā)
3、支持忙閑不均
10.4實(shí)現(xiàn)兩個(gè)消費(fèi)者線程,兩個(gè)生產(chǎn)者線程的生產(chǎn)者消費(fèi)者模型
生產(chǎn)者生成時(shí)用的同一個(gè)全局變量,故對該全局變量進(jì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ù),消費(fèi)者線程使用的
{
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;//隊(duì)列
pthread_mutex_t _mutex;//互斥鎖
pthread_cond_t _cons;//消費(fèi)者條件變量
pthread_cond_t _prod;//生產(chǎn)者條件變量
};
void *ConsumerStart(void *arg)//消費(fèi)者入口函數(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);//++的時(shí)候,給該全局變量加鎖
++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.寫多線程時(shí)應(yīng)注意
先考慮代碼的核心邏輯(先實(shí)現(xiàn))
考慮核心邏輯中是否訪問臨界資源或者說執(zhí)行臨界區(qū)代碼,如果有就需要保持互斥
考慮線程之間是否需要同步
編輯:jq
-
寄存器
+關(guān)注
關(guān)注
31文章
5317瀏覽量
120010 -
cpu
+關(guān)注
關(guān)注
68文章
10825瀏覽量
211151 -
代碼
+關(guān)注
關(guān)注
30文章
4748瀏覽量
68356
原文標(biāo)題:多線程詳解,一篇文章徹底搞懂多線程中各個(gè)難點(diǎn)
文章出處:【微信號:mcu168,微信公眾號:硬件攻城獅】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論