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

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

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

一文帶你徹底搞懂多線程中各個(gè)難點(diǎn)

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

1.什么是線程?

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

1.1線程是怎樣描述的?

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

即:

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

#include 《sys/syscall.h》

int TID = syscall(SYS_gettid);

則對(duì)線程組而言,所有的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)用?;靵y的問題?

工作線程和主線程共用一個(gè)mm_struct,如果都向棧中壓棧,必然會(huì)導(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

信號(hào)屏蔽字

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

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

文件描述符表

信號(hào)的處理方式

當(dāng)前工作目錄

用戶id和組id

1.4為什么要有多線程?

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

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

1、線程會(huì)共享內(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)程之間的共享要簡(jiǎn)單。

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

1.4.2線程帶來的缺點(diǎn)

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

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

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è)線程都會(huì)鏈入到該雙向鏈表中。

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

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

線程的對(duì)等關(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)識(shí)符,是一個(gè)出參

2、attr:線程屬性

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

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

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

詳解:

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

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

第三個(gè)參數(shù)是線程需要執(zhí)行的函數(shù)。創(chuàng)建線程, 是為了讓線程執(zhí)行一定的任務(wù)。線程創(chuàng)建成功之后, 該線程就會(huì)執(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是否對(duì)應(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ù)為線程分配棧空間。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就有可能會(huì)被復(fù)用:

1、線程退出。

2、線程組的其他線程對(duì)該線程執(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ù)會(huì)將線程的屬性重置成默認(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)程不會(huì)終止的方法:

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)用的任何層級(jí)的函數(shù)執(zhí)行pthread_exit() 都會(huì)引發(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)識(shí)符

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

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

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)的成員是對(duì)等的關(guān)系, 只要是在一個(gè)線程組內(nèi), 就可以對(duì)另外一個(gè)線程執(zhí)行連接(join) 操作。

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

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

如果不連接已經(jīng)退出的線程, 會(huì)導(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會(huì)影響性能, 所以將該棧緩存起來, 放到一個(gè)鏈表之中, 如果有新的創(chuàng)建線程的請(qǐng)求, 會(huì)首先在棧緩存鏈表中尋找空間合適的棧, 有的話, 直接將該棧分配給新創(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)的線程退出后, 需要對(duì)其執(zhí)行連接操作, 否則線程資源無法釋放, 從而造成資源泄漏。

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

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

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

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

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ù)都要對(duì)這個(gè)全局變量進(jìn)行–操作。

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

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

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

上述例子中,線程A和B都對(duì)全局變量進(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è)正確并且高效的鎖非常困難。縱然拋下高效不談, 讓用戶從零開始實(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)銷毀的互斥量, 要確保后面不會(huì)有線程再嘗試加鎖。

當(dāng)互斥量處于已加鎖的狀態(tài), 或者正在和條件變量配合使用, 調(diào)用pthread_mutex_destroy函數(shù)會(huì)返回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)中的值會(huì)被更改為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í),加鎖失敗,但也會(huì)返回,此時(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í)候,會(huì)等待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);

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

解鎖的時(shí)候,會(huì)將互斥鎖當(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è)線程,對(duì)同一個(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ū)的線程競(jìng)爭(zhēng)(搶鎖), 所以互斥量并沒有做到先來先服務(wù)。

6.7互斥鎖的類型

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

2、

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

3、

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

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

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

6.8死鎖和活鎖

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

6.8.1死鎖概念

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

6.8.2死鎖的四個(gè)必要條件

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

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

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

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

6.8.3避免死鎖

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

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

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

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

6.8.5死鎖調(diào)試

查看多個(gè)線程堆棧:thread apply all bt

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

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

直接從pid號(hào)用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;

}

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

開始調(diào)試:

1、找到進(jìn)程號(hào)

2、開始調(diào)試

3、查看多個(gè)線程堆棧

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

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

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

6.9讀寫鎖

6.9.1什么是讀寫鎖?

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

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

即讀線程可多個(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);

對(duì)于調(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í),會(huì)先對(duì)引用計(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)前讀寫鎖被某寫線程占用著,則不允許任何讀鎖通過請(qǐng)求,也不允許任何寫鎖請(qǐng)求通過,讀鎖請(qǐng)求和寫鎖請(qǐ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讀寫鎖的競(jìng)爭(zhēng)策略

對(duì)于讀寫鎖而言,目前有兩種策略,讀者優(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)先:讀鎖來請(qǐng)求可以立即響應(yīng),只要有一個(gè)讀鎖沒完成,那么寫鎖就無法寫。這種策略是不公平的,極端情況下,寫現(xiàn)場(chǎng)很可能被餓死,即線程總是拿不到鎖資源。

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

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

對(duì)于讀請(qǐng)求而言:如果

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

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

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

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

對(duì)于寫請(qǐng)求而言:如果

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

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

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

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

對(duì)于解鎖,如果當(dāng)前是寫鎖:

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

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

對(duì)于解鎖,如果當(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為什么需要線程同步?

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

例如:

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

7.2如何做到線程間同步?

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

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

則在這個(gè)情況下,需要做到:

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

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

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

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è)接口中有互斥鎖?

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

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

pthread_mutex_lock(&m)

while(condition_is_false)

{

pthread_mutex_unlock(&m);

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

cond_wait(&cv);

pthread_mutex_lock(&m);

}

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

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

pthread_mutex_lock(&m);

while(condition_is_false)

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

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

*并且在此持有了互斥量

*/

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

*/

pthread_mutex_unlock(&m);

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

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

while(condition_is_false)

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

if(condition_is_false)

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

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

因?yàn)閱拘阎写嬖谔摷賳拘眩╯purious wakeup) , 換言之,條件尚未滿足, pthread_cond_wait就返了。在一些實(shí)現(xiàn)中, 即使沒有其他線程向條件變量發(fā)送信號(hào), 等待此條件變量的線程也有可能會(huì)醒來。

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

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ù)才會(huì)返回

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ā)送信號(hào),然后解鎖互斥量,這個(gè)順序是必須的嘛?

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

7.3.4條件變量的銷毀

int pthread_cond_destroy(pthread_cond_t *cond);

注意:

1、永遠(yuǎn)不要用一個(gè)條件變量對(duì)另一個(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è)條件變量,會(huì)導(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)者,則必會(huì)搶到鎖,消費(fèi)材料,喚醒PCB等待隊(duì)列,若此時(shí)喚醒的是,消費(fèi)者,則現(xiàn)在是這樣一個(gè)情況:

4、兩個(gè)消費(fèi)者在外邊搶鎖,一定都會(huì)進(jìn)入PCB等待隊(duì)列中

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

1、使用int pthread_cond_broadcast(pthread_cond_t *cond);,喚醒PCB等待隊(duì)列中所有的線程。此時(shí)所有線程都會(huì)同時(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ā)送取消請(qǐng)求。這不是個(gè)阻塞型接口, 發(fā)出請(qǐng)求后, 函數(shù)就立刻返回了, 而不會(huì)等待目標(biāo)線程退出之后才返回。

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

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

什么是取消點(diǎn)?可通過man pthreads查看取消點(diǎn)

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

8.2線程取消帶來的弊端

目標(biāo)線程可能會(huì)持有互斥量、 信號(hào)量或其他類型的鎖, 這時(shí)候如果收到取消請(qǐng)求, 并且取消類型是異步取消, 那么可能目標(biāo)線程掌握的資源還沒有來得及釋放就被迫退出了, 這可能會(huì)給其他線程帶來不可恢復(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è)遇到取消請(qǐng)求, 線程執(zhí)行到了取消點(diǎn), 卻沒有來得及做清理動(dòng)作(如動(dòng)態(tài)申請(qǐng)的內(nèi)存沒有釋放, 申請(qǐng)的互斥量沒有解鎖等) , 可能會(huì)導(dǎo)致錯(cuò)誤的產(chǎn)生, 比如死鎖, 甚至是進(jìn)程崩潰。

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

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

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

void pthread_cleanup_pop(int execute);

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

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

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

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

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

4、線程的主函數(shù)是通過return返回的, 并且pthread_cleanup_pop的唯一參數(shù)execute是非零值時(shí), 清理函數(shù)會(huì)執(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);//測(cè)試該線程是否可以拿到鎖

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ù), 會(huì)復(fù)制一個(gè)進(jìn)程, 對(duì)于多線程程序而言, fork函數(shù)復(fù)制的是用fork的那個(gè)線程, 而并不復(fù)制其他的線程。fork之后其他線程都不見了。Linux存在forkall語義的系統(tǒng)調(diào)用, 無法做到將多線程全部復(fù)制。

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

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è)全局變量,故對(duì)該全局變量進(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

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

    關(guān)注

    31

    文章

    5301

    瀏覽量

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

    關(guān)注

    68

    文章

    10813

    瀏覽量

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

    關(guān)注

    30

    文章

    4728

    瀏覽量

    68250

原文標(biāo)題:多線程詳解,一篇文章徹底搞懂多線程中各個(gè)難點(diǎn)

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

收藏 人收藏

    評(píng)論

    相關(guān)推薦

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

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

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

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

    搞懂Linux進(jìn)程的睡眠和喚醒

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

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

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

    掌握Python多線程

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

    多線程設(shè)計(jì)模式到對(duì) CompletableFuture 的應(yīng)用

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

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

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

    python5種線程鎖盤點(diǎn)

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

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

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

    你還是分不清多進(jìn)程和多線程嗎?搞懂!

    你還是分不清多進(jìn)程和多線程嗎?搞懂! 多進(jìn)程和多線程是并發(fā)編程中常見的兩個(gè)概念,它們都可以用于提高程序的性能和效率。但是它們的實(shí)現(xiàn)方式和
    的頭像 發(fā)表于 12-19 16:07 ?527次閱讀

    redis多線程還能保證線程安全嗎

    是單線程的,多個(gè)客戶端請(qǐng)求會(huì)按序執(zhí)行,每個(gè)請(qǐng)求使用個(gè)線程完成,這樣可以避免多線程之間的競(jìng)爭(zhēng)條件和鎖等帶來的開銷。但是,由于Redis是存儲(chǔ)內(nèi)存
    的頭像 發(fā)表于 12-05 10:28 ?1640次閱讀

    mfc多線程編程實(shí)例

    (圖形用戶界面)應(yīng)用程序的開發(fā)。在這篇文章,我們將重點(diǎn)介紹MFC多線程編程。 多線程編程在軟件開發(fā)中非常重要,它可以實(shí)現(xiàn)程序的并發(fā)執(zhí)行,提高程序的效率和響應(yīng)速度。MFC提供了豐富
    的頭像 發(fā)表于 12-01 14:29 ?1379次閱讀

    多線程如何保證數(shù)據(jù)的同步

    多線程編程是種并發(fā)編程的方法,意味著程序同時(shí)運(yùn)行多個(gè)線程,每個(gè)線程可獨(dú)立執(zhí)行不同的任務(wù),共享同
    的頭像 發(fā)表于 11-17 14:22 ?1115次閱讀

    多線程并發(fā)查詢oracle數(shù)據(jù)庫

    數(shù)據(jù)庫的原理、使用場(chǎng)景、實(shí)現(xiàn)方法以及可能遇到的問題和解決方案。 多線程并發(fā)查詢的原理 在傳統(tǒng)的單線程查詢方式,當(dāng)個(gè)查詢請(qǐng)求發(fā)起時(shí),數(shù)
    的頭像 發(fā)表于 11-17 14:22 ?3577次閱讀

    多線程同步的幾種方法

    多線程同步是指在多個(gè)線程并發(fā)執(zhí)行的情況下,為了保證線程執(zhí)行的正確性和致性,需要采用特定的方法來協(xié)調(diào)線程之間的執(zhí)行順序和共享資源的訪問。下面
    的頭像 發(fā)表于 11-17 14:16 ?1108次閱讀