Linux多線程-互斥和同步
@TOC
零、前言
本章主要講解學(xué)習(xí)linux中對多線程的執(zhí)行中的互斥與安全問題
一、linux線程互斥
1、基本概念及引入
互斥相關(guān)概念:
臨界資源:多線程執(zhí)行流共享的資源就叫做臨界資源
臨界區(qū):每個(gè)線程內(nèi)部,訪問臨界資源的代碼,就叫做臨界區(qū)
互斥:任何時(shí)刻,互斥保證有且只有一個(gè)執(zhí)行流進(jìn)入臨界區(qū),訪問臨界資源,通常對臨界資源起保護(hù)作用
原子性:不會被任何調(diào)度機(jī)制打斷的操作,該操作只有兩態(tài),要么完成,要么未完成
示例:模擬搶票
#include
效果:
注:變量tickets被多個(gè)執(zhí)行流同時(shí)訪問,所以thickets就是一個(gè)臨界資源,當(dāng)訪問臨界資源時(shí),判斷tickets是否大于0、打印剩余票數(shù)以及--tickets的代碼也就是臨界區(qū)
出現(xiàn)負(fù)數(shù)的原因:
if語句判斷條件為真以后,代碼可以并發(fā)的切換到其他線程
usleep用于模擬漫長業(yè)務(wù)的過程,在這個(gè)漫長的業(yè)務(wù)過程中,可能有很多個(gè)線程會進(jìn)入該代碼段
–ticket操作本身就不是一個(gè)原子操作,可能在執(zhí)行當(dāng)中也被切換成其他線程
具體可能的過程:
當(dāng)thickets為1時(shí),一個(gè)線程進(jìn)行if判斷為真,進(jìn)入代碼段,當(dāng)執(zhí)行到usleep進(jìn)行系統(tǒng)調(diào)用休眠,返回時(shí)到用戶態(tài)時(shí)線程發(fā)生切換,多個(gè)線程此時(shí)也進(jìn)行if判斷為真(thickets還是1),這些線程當(dāng)進(jìn)行打印的時(shí)候進(jìn)行了多次的減減操作,也就造成了負(fù)數(shù)的情況
– 操作并不是原子操作,而是對應(yīng)三條匯編指令:
load :將共享變量ticket從內(nèi)存加載到寄存器中
update : 更新寄存器里面的值,執(zhí)行-1操作
store :將新值,從寄存器寫回共享變量ticket的內(nèi)存地址
–執(zhí)行對應(yīng)的匯編代碼:
152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 600b34 152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 600b34
注:因?yàn)闇p減操作并不是原子的,當(dāng)減減操作第一步執(zhí)行完(thickets=100),可能該線程的時(shí)間片到了(寄存器中的數(shù)據(jù)被保存eax=100),其他線程切入,而切入的線程執(zhí)行了多次減減并寫會到內(nèi)存(thickets=80),當(dāng)切出的線程切回時(shí),恢復(fù)線程上下文數(shù)據(jù)(eax=100),再進(jìn)行減減(eax=99),把數(shù)據(jù)寫回到內(nèi)存時(shí)(thickets=99),此時(shí)的數(shù)據(jù)的值只達(dá)到了一次減減的效果,此時(shí)的資源并不安全
2、互斥量mutex介紹
概念:
大部分情況,線程使用的數(shù)據(jù)都是局部變量,變量的地址空間在線程棧空間內(nèi),這種情況變量歸屬單個(gè)線程,其他線程無法獲得這種變量
但有時(shí)候,很多變量都需要在線程間共享,這樣的變量成為共享變量,可以通過數(shù)據(jù)的共享,完成線程之間的交互
多個(gè)線程并發(fā)的操作共享變量,就會帶來一些問題
要解決以上問題需要做到三點(diǎn):
代碼必須要有互斥行為:當(dāng)代碼進(jìn)入臨界區(qū)執(zhí)行時(shí),不允許其他線程進(jìn)入該臨界區(qū)
如果多個(gè)線程同時(shí)要求執(zhí)行臨界區(qū)的代碼,并且臨界區(qū)沒有線程在執(zhí)行,那么只能允許一個(gè)線程進(jìn)入該臨界區(qū)
如果線程不在臨界區(qū)中執(zhí)行,那么該線程不能阻止其他線程進(jìn)入臨界區(qū)
注:要做到這三點(diǎn),本質(zhì)上就是需要一把鎖,Linux上提供的這把鎖叫互斥量
示圖:
3、互斥量的使用
初始化互斥量:
靜態(tài)分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
動(dòng)態(tài)分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrictattr);
參數(shù):mutex:要初始化的互斥量;attr:互斥量的屬性,一般設(shè)置為NULL
銷毀互斥量:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
注意:
使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要銷毀
不要銷毀一個(gè)已經(jīng)加鎖的互斥量
已經(jīng)銷毀的互斥量,要確保后面不會有線程再嘗試加鎖
互斥量加鎖和解鎖:
int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失敗返回錯(cuò)誤號
調(diào)用 pthread_ lock 時(shí)可能遇到的情況:
互斥量處于未鎖狀態(tài),該函數(shù)會將互斥量鎖定,同時(shí)返回成功
發(fā)起函數(shù)調(diào)用時(shí),其他線程已經(jīng)鎖定互斥量,或者存在其他線程同時(shí)申請互斥量,但沒有競爭到互斥量,那么pthread_ lock調(diào)用會陷入阻塞(執(zhí)行流被掛起),等待互斥量解鎖
示例:改進(jìn)搶票
#include
效果:
4、互斥量原理
概念:
對于互斥鎖來說被多個(gè)線程同時(shí)可見,也就是說互斥鎖本身就是一個(gè)臨界資源,所以互斥鎖想要保護(hù)臨界區(qū)的互斥性,那么互斥鎖操作則一定是原子的
為了實(shí)現(xiàn)互斥鎖操作,大多數(shù)體系結(jié)構(gòu)都提供了swap或exchange指令,該指令的作用是把寄存器和內(nèi)存單元的數(shù)據(jù)相交換,由于只有一條指令,保證了原子性
即使是多處理器平臺,訪問內(nèi)存的總線周期也有先后,一個(gè)處理器上的交換指令執(zhí)行時(shí)另一個(gè)處理器的交換指令只能等待總線周期
示圖:偽代碼
注:在交換和賦值的過程中本質(zhì)就是讓競爭的多線程中保證中有一個(gè)線程的交換得到的寄存器數(shù)據(jù)為1,即保證同一時(shí)刻只有一個(gè)競爭的線程為1,由此才能往下執(zhí)行,否則只能進(jìn)行等待
二、可重入/線程安全
1、基本概念
線程安全:
多個(gè)線程并發(fā)同一段代碼時(shí),不會出現(xiàn)不同的結(jié)果,沒有數(shù)據(jù)錯(cuò)亂的情況
常見對全局變量或者靜態(tài)變量進(jìn)行操作,并且沒有鎖保護(hù)的情況下,會出現(xiàn)該問題
重入:
同一個(gè)函數(shù)被不同的執(zhí)行流調(diào)用,當(dāng)前一個(gè)流程還沒有執(zhí)行完,就有其他的執(zhí)行流再次進(jìn)入,我們稱之為重入
一個(gè)函數(shù)在重入的情況下,運(yùn)行結(jié)果不會出現(xiàn)任何不同或者任何問題,則該函數(shù)被稱為可重入函數(shù),否則是不可重入函數(shù)
注意:
對于可重入來說是函數(shù)的特性,對于線程安全來說是線程的特性
如果一個(gè)函數(shù)是可重入的,那么執(zhí)行還函數(shù)的多線程是線程安全的
2、線程安全
常見線程不安全的情況:
不保護(hù)共享變量的函數(shù)
函數(shù)狀態(tài)隨著被調(diào)用,狀態(tài)發(fā)生變化的函數(shù)
返回指向靜態(tài)變量指針的函數(shù)
調(diào)用線程不安全函數(shù)的函數(shù)
常見的線程安全的情況:
每個(gè)線程對全局變量或者靜態(tài)變量只有讀取的權(quán)限,而沒有寫入的權(quán)限,一般來說這些線程是安全的
類或者接口對于線程來說都是原子操作
多個(gè)線程之間的切換不會導(dǎo)致該接口的執(zhí)行結(jié)果存在二義性
3、重入函數(shù)
常見不可重入的情況:
調(diào)用了malloc/free函數(shù),因?yàn)閙alloc函數(shù)是用全局鏈表來管理堆的
調(diào)用了標(biāo)準(zhǔn)I/O庫函數(shù),標(biāo)準(zhǔn)I/O庫的很多實(shí)現(xiàn)都以不可重入的方式使用全局?jǐn)?shù)據(jù)結(jié)構(gòu)
可重入函數(shù)體內(nèi)使用了靜態(tài)的數(shù)據(jù)結(jié)構(gòu)
常見可重入的情況:
不使用全局變量或靜態(tài)變量
不使用用malloc或者new開辟出的空間
不調(diào)用不可重入函數(shù)
不返回靜態(tài)或全局?jǐn)?shù)據(jù),所有數(shù)據(jù)都有函數(shù)的調(diào)用者提供
使用本地?cái)?shù)據(jù),或者通過制作全局?jǐn)?shù)據(jù)的本地拷貝來保護(hù)全局?jǐn)?shù)據(jù)
4、聯(lián)系與區(qū)別
可重入與線程安全聯(lián)系:
函數(shù)是可重入的,那就是線程安全的
函數(shù)是不可重入的,那就不能由多個(gè)線程使用,有可能引發(fā)線程安全問題
如果一個(gè)函數(shù)中有全局變量,那么這個(gè)函數(shù)既不是線程安全也不是可重入的
可重入與線程安全區(qū)別:
可重入函數(shù)是線程安全函數(shù)的一種
線程安全不一定是可重入的,而可重入函數(shù)則一定是線程安全的
如果將對臨界資源的訪問加上鎖,則這個(gè)函數(shù)是線程安全的,但如果這個(gè)重入函數(shù)若鎖還未釋放則會產(chǎn)生死鎖,因此是不可重入的
三、常見鎖概念
死鎖:
死鎖是指在一組進(jìn)程中的各個(gè)進(jìn)程均占有不會釋放的資源,但因互相申請被其他進(jìn)程所站用不會釋放的資源而處于的一種永久等待狀態(tài)
死鎖四個(gè)必要條件:
互斥條件:一個(gè)資源每次只能被一個(gè)執(zhí)行流使用
請求與保持條件:一個(gè)執(zhí)行流因請求資源而阻塞時(shí),對已獲得的資源保持不放
不剝奪條件:一個(gè)執(zhí)行流已獲得的資源,在末使用完之前,不能強(qiáng)行剝奪
循環(huán)等待條件:若干執(zhí)行流之間形成一種頭尾相接的循環(huán)等待資源的關(guān)系
注:對于死鎖,四個(gè)條件缺一不可
避免死鎖:
破壞死鎖的四個(gè)必要條件
加鎖順序一致
避免鎖未釋放的場景
資源一次性分配
避免死鎖算法:
死鎖檢測算法
銀行家算法
四、Linux線程同步
1、基本概念
同步概念與競態(tài)條件:
同步:在保證數(shù)據(jù)安全的前提下,讓線程能夠按照某種特定的順序訪問臨界資源,從而有效避免饑餓問題,叫做同步
競態(tài)條件:因?yàn)闀r(shí)序問題,而導(dǎo)致程序異常,我們稱之為競態(tài)條件
注意:
在多線程中,為了保護(hù)臨界資源,我們需要用到互斥鎖,但是在線程競爭的情況下,此外我們還需要考慮資源的一些特殊情況
在特殊的情況下,可能存在某個(gè)線程多次的競爭獲取鎖,但是卻沒有做出實(shí)際的事情,這種頻繁的申請雖然沒有什么問題,但是不是很合理
同時(shí)如果線程的競爭力非常強(qiáng),這就可能導(dǎo)致其他線程長時(shí)間競爭不到鎖,引起饑餓問題
由此我們需要對于這種特殊的情況,保證線程能夠按照某種次序進(jìn)行臨界資源的訪問,由此就需要條件變量
條件變量:
當(dāng)一個(gè)線程互斥地訪問某個(gè)變量時(shí),它可能發(fā)現(xiàn)在其它線程改變狀態(tài)之前,它什么也做不了。例如一個(gè)線程訪問隊(duì)列時(shí),發(fā)現(xiàn)隊(duì)列為空,它只能等待,只到其它線程將一個(gè)節(jié)點(diǎn)添加到隊(duì)列中
2、條件變量的使用
初始化條件變量:
靜態(tài)分配
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
動(dòng)態(tài)分配
初始化函數(shù)原型:
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrictattr);
解釋:
參數(shù):cond:要初始化的條件變量;attr:設(shè)置屬性,一般填NULL
返回值:條件變量初始化成功返回0,失敗返回錯(cuò)誤碼
銷毀函數(shù)原型:
int pthread_cond_destroy(pthread_cond_t *cond)
解釋:
參數(shù):cond:需要銷毀的條件變量
返回值:條件變量銷毀成功返回0,失敗返回錯(cuò)誤碼
使用PTHREAD_COND_INITIALIZER初始化的條件變量不需要銷毀
等待條件滿足函數(shù)原型:
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
解釋:
功能:進(jìn)行等待直到條件符合被喚醒
參數(shù):cond:需要等待的條件變量;mutex:當(dāng)前線程所處臨界區(qū)對應(yīng)的互斥鎖
返回值:函數(shù)調(diào)用成功返回0,失敗返回錯(cuò)誤碼
喚醒等待函數(shù)原型:
int pthread_cond_broadcast(pthread_cond_t *cond); int pthread_cond_signal(pthread_cond_t *cond);
解釋:
區(qū)別:pthread_cond_signal函數(shù)用于喚醒等待隊(duì)列中首個(gè)線程;pthread_cond_broadcast函數(shù)用于喚醒等待隊(duì)列中的全部線程
參數(shù):cond:喚醒在cond條件變量下等待的線程
返回值:函數(shù)調(diào)用成功返回0,失敗返回錯(cuò)誤碼
示例:協(xié)同調(diào)度其他線程
#include
效果:
3、條件變量等待
為什么條件變量等待函數(shù)第二個(gè)參數(shù)需要互斥鎖:
條件等待是線程間同步的一種手段,如果只有一個(gè)線程,條件不滿足,一直等下去都不會滿足,所以必須要有一個(gè)線程通過某些操作,改變共享變量,使原先不滿足的條件變得滿足,并且友好的通知等待在條件變量上的線程
條件不會無緣無故的突然變得滿足了,必然會牽扯到共享數(shù)據(jù)的變化,所以一定要用互斥鎖來保護(hù),沒有互斥鎖就無法安全的獲取和修改共享數(shù)據(jù)
進(jìn)入訪問臨界資源時(shí),申請互斥鎖,當(dāng)遇到條件變量等待時(shí),傳入第二個(gè)參數(shù)互斥鎖,等待的同時(shí)會將所申請到的互斥鎖給釋放,被喚醒的時(shí)候會同時(shí)將互斥鎖給競爭上,保證數(shù)據(jù)安全
示圖:
注:如果不釋放互斥鎖,那么其他線程無法成功申請到鎖進(jìn)而改變數(shù)據(jù),也就沒有辦法通知等待的線程,那么申請到鎖的線程一直等待,別的線程無法獲取鎖也無法通知,也就會造成死鎖
錯(cuò)誤偽代碼設(shè)計(jì):訪問臨界資源時(shí),先上鎖,發(fā)現(xiàn)條件不滿足,解鎖,然后等待在條件變量上
pthread_mutex_lock(&mutex); while (condition_is_false) { pthread_mutex_unlock(&mutex); //解鎖之后,等待之前,條件可能已經(jīng)滿足,信號已經(jīng)發(fā)出,但是該信號可能被錯(cuò)過 pthread_cond_wait(&cond); pthread_mutex_lock(&mutex); } pthread_mutex_unlock(&mutex);
注意:
這里由于解鎖和等待不是原子操作。調(diào)用解鎖之后, pthread_cond_wait 之前,如果已經(jīng)有其他線程獲取到互斥量,并且條件滿足,發(fā)送了喚醒信號,那么 pthread_cond_wait 將錯(cuò)過這個(gè)信號,可能會導(dǎo)致線程永遠(yuǎn)阻塞在這個(gè) pthread_cond_wait ,所以解鎖和等待必須是一個(gè)原子操作
調(diào)用pthread_cond_wait函數(shù)會去看條件量是否等于0:如果等于,就把互斥量改為1,直到cond_ wait返回,把條件量改成1,把互斥量恢復(fù)成原樣,也就是不滿足條件時(shí),在進(jìn)行等待前,把互斥鎖給解鎖,當(dāng)?shù)却奖粏拘褧r(shí)會自動(dòng)競爭到互斥鎖
4、條件變量使用規(guī)范
等待條件代碼
pthread_mutex_lock(&mutex); while (條件為假){ pthread_cond_wait(cond, mutex); } //修改條件 pthread_mutex_unlock(&mutex);
注:這里可能存在被偽喚醒的情況,當(dāng)喚醒的時(shí)候可能競爭鎖失敗,繼續(xù)等待,其他線程競爭成功執(zhí)行后并釋放鎖,此時(shí)條件判斷為假,但是該線程競爭到鎖后會繼續(xù)往下執(zhí)行,如果沒有再次進(jìn)行判斷可能造成錯(cuò)誤,使用while循環(huán)判斷保證醒來后條件一定為真才往下走
給條件發(fā)送信號代碼
pthread_mutex_lock(&mutex); //設(shè)置條件為真 pthread_cond_signal(cond); pthread_mutex_unlock(&mutex);
五、POSIX信號量
1、信號量概念及介紹
基本概念:
POSIX信號量和SystemV信號量作用相同,都是用于同步操作,達(dá)到無沖突的訪問共享資源目的。 但POSIX可以用于線程間同步
信號量本質(zhì)是一個(gè)描述臨界資源中資源數(shù)目的計(jì)數(shù)器,信號量能夠更細(xì)粒度的對臨界資源進(jìn)行管理,每個(gè)執(zhí)行流在進(jìn)入臨界區(qū)之前都應(yīng)該先申請信號量,申請成功就有了訪問臨界資源的權(quán)限,當(dāng)訪問離開就進(jìn)行釋放信號量(類似一個(gè)訪問預(yù)定機(jī)制)
一般來說我們是將臨界資源作為一個(gè)整體看待,所以需要使用互斥鎖讓同一時(shí)刻只能有一個(gè)執(zhí)行流進(jìn)行訪問臨界資源;實(shí)際對于臨界資源我們可以選擇分割為多個(gè)區(qū)域,當(dāng)多個(gè)執(zhí)行流需要訪問不同區(qū)域的臨界資源時(shí),那么我們可以讓這些執(zhí)行流同時(shí)訪問臨界資源的不同區(qū)域,此時(shí)不會出現(xiàn)數(shù)據(jù)不一致等問題
信號量的PV操作:
P操作:申請信號量獲得臨界資源中某塊資源的使用權(quán)限,當(dāng)申請成功時(shí)邏輯上臨界資源中可使用資源數(shù)目減一,對應(yīng)到信號量上就是讓計(jì)數(shù)器減一
V操作:釋放信號量歸還臨界資源中某塊資源的使用權(quán)限,當(dāng)釋放成功時(shí)邏輯上臨界資源中可使用的資源數(shù)目加一,對應(yīng)到信號量上就是讓計(jì)數(shù)器加一
注意:
信號量本質(zhì)也是臨界資源(被多個(gè)執(zhí)行流申請),要保護(hù)臨界資源所以信號量的PV操作必須是原子操作
當(dāng)臨界資源申請完時(shí),信號量為0,再申請時(shí)線程會在該信號量的等待隊(duì)列當(dāng)中進(jìn)行等待,直到有信號量被釋放時(shí)再被喚醒
二元信號量:
如果將信號量的初始值設(shè)置為1,那么此時(shí)該信號量叫做二元信號量
信號量的初始值為1,說明信號量所描述的臨界資源只有一份,此時(shí)信號量的作用基本等價(jià)于互斥鎖
2、信號量的使用
初始化信號量函數(shù)原型:
#include
解釋:
參數(shù):sem:需要初始化的信號量;pshared:0表示線程間共享,非零表示進(jìn)程間共享;value:信號量初始值
返回值:初始化信號量成功返回0,失敗返回-1
銷毀信號量函數(shù)原型:
int sem_destroy(sem_t *sem);
解釋:
參數(shù):sem:需要銷毀的信號量
返回值:銷毀信號量成功返回0,失敗返回-1
等待信號量函數(shù)原型:
int sem_wait(sem_t *sem); //P()
解釋:
功能:等待信號量,會將信號量的值減1
參數(shù):sem:需要等待的信號量
返回值:等待信號量成功返回0,信號量的值減一;等待信號量失敗返回-1,信號量的值保持不變
發(fā)布信號量函數(shù)原型:
int sem_post(sem_t *sem);//V()
解釋:
功能:發(fā)布信號量,表示資源使用完畢可以歸還資源了,將信號量值加1
參數(shù):sem:需要發(fā)布的信號量
返回值:發(fā)布信號量成功返回0,信號量的值加一;發(fā)布信號量失敗返回-1,信號量的值保持不變
示例:
#include
效果:
Linux 任務(wù)調(diào)度 多線程
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。