「前言」文章是關(guān)于Linux多線程方面的知識(shí),上一篇是?Linux多線程詳解(二),今天這篇是 Linux多線程詳解(三),內(nèi)容大致是線程互斥與線程同步,講解下面開始!
「歸屬專欄」Linux系統(tǒng)編程
「主頁鏈接」個(gè)人主頁
「筆者」楓葉先生(fy)
「楓葉先生有點(diǎn)文青病」「每篇一句」
滿堂花醉三千客,
一劍霜寒十四州。
——貫休《獻(xiàn)錢尚父》
目錄
四、Linux線程互斥
4.1?進(jìn)程線程間的互斥相關(guān)概念
4.2?互斥量mutex
4.3?互斥量接口函數(shù)
4.4?互斥量實(shí)現(xiàn)原理
五、可重入和線程安全
5.1?概念
5.2?常見的線程不安全的情況
5.3?常見的線程安全的情況
5.4?常見不可重入的情況
5.5 常見可重入的情況
5.6?可重入與線程安全聯(lián)系
5.7?可重入與線程安全區(qū)別
六、死鎖
6.1 概念
6.2?死鎖四個(gè)必要條件
6.3?避免死鎖
七、Linux線程同步
7.1?同步概念與競(jìng)態(tài)條件
7.2 條件變量
7.3 條件變量相關(guān)函數(shù)
四、Linux線程互斥
4.1?進(jìn)程線程間的互斥相關(guān)概念
- 臨界資源:多線程執(zhí)行流共享的資源就叫做臨界資源
- 臨界區(qū):每個(gè)線程內(nèi)部,訪問臨界資源的代碼,就叫做臨界區(qū)
- 互斥:任何時(shí)刻,互斥保證有且只有一個(gè)執(zhí)行流進(jìn)入臨界區(qū),訪問臨界資源,通常對(duì)臨界資源起保護(hù)作用
- 原子性:不會(huì)被任何調(diào)度機(jī)制打斷的操作,該操作只有兩態(tài),要么完成,要么未完成
臨界資源&&臨界區(qū)
如何理解臨界資源和臨界區(qū)??
在前面進(jìn)程間通信,進(jìn)程間想要通信,必須得依賴第三方資源,因?yàn)檫M(jìn)程之間是互相獨(dú)立的,第三方資源比如是:管道、共享內(nèi)存等等。進(jìn)程間通信中的第三方資源就叫做臨界資源,訪問第三方資源的代碼就叫做臨界區(qū)。第三方資源也叫共享資源
而線程之間想要通信則比較簡(jiǎn)單,因?yàn)榫€程之間的大部分資源都是共享的。比如,定義一個(gè)全局變量 ticket,這個(gè)ticket就是一個(gè)共享資源,每個(gè)線程都可以看得到這份資源。
假設(shè)ticket是電影售票系統(tǒng)中的一種電影的票,ticket 一共有1000張,現(xiàn)在有兩個(gè)線程進(jìn)行對(duì)該電影票進(jìn)行搶票行為,代碼如下:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
// 票 -- 共享資源
int tickets = 1000;
void* getTicket(void* args)
{
string username = static_cast<const char*>(args);
while(1)
{
if(tickets > 0)
{
cout << username << ": 正在進(jìn)行搶票 " << tickets-- << endl;
sleep(1);//模擬搶票
}
else
{
break;
}
}
}
int main()
{
pthread_t tid1, tid2;
pthread_create(&tid1, nullptr, getTicket, (void*)"thread 1");
pthread_create(&tid2, nullptr, getTicket, (void*)"thread 2");
pthread_join(tid1, nullptr);
pthread_join(tid2, nullptr);
return 0;
}
編譯運(yùn)行,兩個(gè)線程都可以進(jìn)行搶票
在上面的例子中,全局變量 tickets 就是臨界資源,每個(gè)線程中對(duì)臨界資源進(jìn)行訪問的代碼稱為臨界區(qū)
原子性與互斥
在多線程情況下,如果這多個(gè)執(zhí)行流都自顧自的對(duì)臨界資源進(jìn)行操作,那么此時(shí)就可能導(dǎo)致數(shù)據(jù)不一致的問題,會(huì)產(chǎn)生線程安全的問題。
比如,多個(gè)線程進(jìn)行搶票,主線程創(chuàng)建5個(gè)線程進(jìn)行搶票,票數(shù)為0線程就自動(dòng)結(jié)束了,主線程只需 join 即可
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
// 票 -- 共享資源
int tickets = 1000;
void* getTicket(void* args)
{
string username = static_cast<const char*>(args);
while(1)
{
if(tickets > 0)
{
//模擬搶票花費(fèi)的時(shí)間
usleep(12345);//微秒
cout << username << ": 正在進(jìn)行搶票 " << tickets-- << endl;
}
else
{
break;
}
}
}
int main()
{
pthread_t tid1, tid2, tid3, tid4, tid5;
pthread_create(&tid1, nullptr, getTicket, (void*)"thread 1");
pthread_create(&tid2, nullptr, getTicket, (void*)"thread 2");
pthread_create(&tid3, nullptr, getTicket, (void*)"thread 3");
pthread_create(&tid4, nullptr, getTicket, (void*)"thread 4");
pthread_create(&tid5, nullptr, getTicket, (void*)"thread 5");
pthread_join(tid1, nullptr);
pthread_join(tid2, nullptr);
pthread_join(tid3, nullptr);
pthread_join(tid4, nullptr);
pthread_join(tid5, nullptr);
return 0;
}
編譯運(yùn)行,多運(yùn)行幾次,我們發(fā)現(xiàn)票數(shù)居然變成負(fù)數(shù)了。票數(shù)本來就1000張,你還賣出了1001、1002、1003、1004張,這明顯不合理,這就是多個(gè)線程同時(shí)訪問一塊資源,帶來的線程安全的問題
- 上面的線程就是交叉執(zhí)行
- 多個(gè)線程交叉執(zhí)行本質(zhì):就是讓調(diào)度器盡可能的頻繁發(fā)生線程調(diào)度與切換
- 線程一般發(fā)生切換:時(shí)間片到了,來了更高優(yōu)先級(jí)的線程,線程等待的時(shí)候。
- 線程是在什么時(shí)候檢測(cè)上面的問題呢?從內(nèi)核態(tài)返回用戶態(tài)的時(shí)候,線程要對(duì)調(diào)度狀態(tài)進(jìn)行檢測(cè),如果可以,就直接發(fā)生線程切換
上面代碼中 tickets 就是臨界資源,因?yàn)樗欢鄠€(gè)執(zhí)行流同時(shí)訪問,而判斷tickets是否大于0、打印剩余票數(shù)以及對(duì)票數(shù)進(jìn)行 --,這些代碼就是臨界區(qū),因?yàn)檫@些代碼對(duì)臨界資源進(jìn)行了訪問
剩余票數(shù)出現(xiàn)負(fù)數(shù)的原因:
- 臨界區(qū)可以被多個(gè)線程進(jìn)行并發(fā)(同時(shí))訪問,臨界資源沒有受到保護(hù)
- usleep 這個(gè)模擬漫長業(yè)務(wù)的過程,在這個(gè)漫長的業(yè)務(wù)過程中,可能有很多個(gè)線程會(huì)進(jìn)入該代碼段
- tickets-- 操作不是原子性的
如何對(duì)臨界區(qū)進(jìn)行保護(hù)??
進(jìn)行互斥,互斥的作用就是,保證在任何時(shí)候有且只有一個(gè)執(zhí)行流(線程)進(jìn)入臨界區(qū),對(duì)臨界資源進(jìn)行訪問
為什么 tickets-- 操作不是原子性的??
原子性指的是:不會(huì)被任何調(diào)度機(jī)制打斷的操作,該操作只有兩態(tài),要么完成,要么未完成
而 -- 操作并不是原子操作,而是對(duì)應(yīng)三條匯編指令:
- load :將共享變量 ticke t從內(nèi)存加載到寄存器中
- update : 更新寄存器里面的值,執(zhí)行-1操作
- store :將新值,從寄存器寫回共享變量 ticket 的內(nèi)存地址
如果是原子性的,就是要一步完成,沒有分出多步。即對(duì)一個(gè)資源進(jìn)行的操作,如果只用一條匯編就能完成,就為原子性
。作對(duì)應(yīng)的匯編代碼如下:
對(duì)應(yīng)操作如下圖,-- 操作分三步
分析為什么票數(shù)會(huì)變成負(fù)數(shù)?
假設(shè)線程1剛執(zhí)行完? if(tickets > 0) 的判斷,線程1就CPU被切走了,也就是從CPU上剝離下來,切走也要保存該線程的上文數(shù)據(jù),因?yàn)橐粋€(gè)CPU的寄存器只有一套,進(jìn)行線程切換必須要對(duì)該進(jìn)程的上下文數(shù)據(jù)進(jìn)行保存。假設(shè)此時(shí)票數(shù)還剩1張
這時(shí)來了一個(gè)線程2,線程2執(zhí)行完? if(tickets > 0)的判斷后,也執(zhí)行完了 -- 操作,即(1)從內(nèi)存讀取 tickets 到CPU的寄存器中,(2)在寄存器中進(jìn)行邏輯運(yùn)算,(3)重新把更新后的 tickets 寫回內(nèi)存
這時(shí),CPU又開始執(zhí)行線程1,重新把線程1的上下文加載到寄存器當(dāng)中,線程1繼續(xù)執(zhí)行原來的代碼,即準(zhǔn)備執(zhí)行打印、 -- 操作。線程1此時(shí)執(zhí)行 -- 操作時(shí),(1)從內(nèi)存讀取 tickets 到CPU的寄存器中,此時(shí)tickets為0。(2)在寄存器中進(jìn)行邏輯運(yùn)算,-1后tickets變成了-1(3)重新把更新后的 tickets 寫回內(nèi)存,此時(shí)tickets就變成了負(fù)數(shù)
這僅僅是其中的一種情況,如果是 -- 操作的三步其中第一步,線程1就被CPU切走了(這種情況模擬不出來,CPU太快了,只能進(jìn)行口述),假設(shè)票數(shù)是1000
-- 操作需要三個(gè)步驟才能完成,那么就有可能當(dāng)thread1剛把 tickets 的值讀進(jìn)CPU就被切走了,也就是從CPU上剝離下來,假設(shè)此時(shí)thread1讀取到的值就是1000,而當(dāng)thread1被切走時(shí),寄存器中的1000叫做thread1的上下文信息,因此需要被保存起來,之后thread1就被掛起了
假設(shè)此時(shí)thread2被調(diào)度了,由于thread1只進(jìn)行了--
操作的第一步,因此thread2此時(shí)看到tickets的值還是1000,而系統(tǒng)給thread2的時(shí)間片可能較多,導(dǎo)致thread2 一次性執(zhí)行了500次 -- 才被切走,最終tickets由1000減到了500
此時(shí)CPU再把thread1恢復(fù)上來,恢復(fù)的本質(zhì)就是繼續(xù)執(zhí)行thread1的代碼,并且要將thread1曾經(jīng)的上下文信息恢復(fù)出來,此時(shí)寄存器當(dāng)中的值是恢復(fù)出來的1000,然后thread1繼續(xù)執(zhí)行?--
操作的第二步和第三步,最終將999寫回內(nèi)存,這簡(jiǎn)直極天理難容
此時(shí)就可能導(dǎo)致數(shù)據(jù)不一致的問題,發(fā)生了數(shù)據(jù)安全性的問題,這就是多線程產(chǎn)生線程安全的問題。
因此對(duì)一個(gè)變量進(jìn)行--
操作并不是原子的,雖然tickets--
就是一行代碼,但這行代碼被編譯器編譯后本質(zhì)上是三行匯編,對(duì)應(yīng) ++操作也不是原子的
如何解決這些問題??互斥量(互斥鎖)
4.2?互斥量mutex
- 大部分情況,線程使用的數(shù)據(jù)都是局部變量,變量的地址空間在線程獨(dú)立??臻g內(nèi),這種情況的變量歸屬單個(gè)線程
- 但有時(shí)候,很多變量都需要在線程間共享,這樣的變量稱為共享變量,可以通過數(shù)據(jù)的共享,完成線程之間的交互。
- 多個(gè)線程并發(fā)的操作共享變量,會(huì)帶來一些線程安全的問題,比如上面舉例的
要解決以上問題,需要做到三點(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上提供的這把鎖叫互斥量,也叫互斥鎖
4.3?互斥量接口函數(shù)
初始化互斥量
互斥量是需要初始化才能使用的,初初始化互斥量有兩種方法:靜態(tài)分配和動(dòng)態(tài)分配
(1)靜態(tài)分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
(2)動(dòng)態(tài)分配
互斥量初始化的函數(shù)是?pthread_mutex_init,man 3?pthread_mutex_init 查看:
函數(shù):pthread_mutex_init
頭文件: #include <pthread.h>
函數(shù)原型:
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
參數(shù):
第一個(gè)參數(shù)mutex:需要初始化的互斥量
第二個(gè)參數(shù)attr:初始化互斥量的屬性,一般設(shè)置為空即可
返回值:
互斥量初始化成功返回0,失敗返回錯(cuò)誤碼
?銷毀互斥量
互斥量使用完了需要進(jìn)行銷毀,互斥量銷毀函數(shù)是?pthread_mutex_destroy?
函數(shù): pthread_mutex_destroy
頭文件:#include <pthread.h>
函數(shù)原型:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
參數(shù):
mutex:需要銷毀的互斥量
返回值:
互斥量銷毀成功返回0,失敗返回錯(cuò)誤碼
銷毀互斥量需要注意:
- 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要銷毀
- 不要銷毀一個(gè)已經(jīng)加鎖的互斥量
- 已經(jīng)銷毀的互斥量,要確保后面不會(huì)有線程再嘗試加鎖
互斥量加鎖
互斥量加鎖的函數(shù)叫做pthread_mutex_lock,man 3 查看:
函數(shù):pthread_mutex_lock
頭文件:#include <pthread.h>
函數(shù)原型:
int pthread_mutex_lock(pthread_mutex_t *mutex);
參數(shù):
mutex:需要加鎖的互斥量
返回值:
互斥量加鎖成功返回0,失敗返回錯(cuò)誤碼
?互斥量解鎖
互斥量解鎖的函數(shù)叫做?pthread_mutex_unlock?
函數(shù):pthread_mutex_unlock
頭文件:#include <pthread.h>
函數(shù)原型:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
參數(shù):
mutex:需要解鎖的互斥量
返回值:
互斥量解鎖成功返回0,失敗返回錯(cuò)誤碼
注意,使用pthread_mutex_lock,可能會(huì)遇到以下情況:
- 互斥量處于未鎖狀態(tài),該函數(shù)會(huì)將互斥量鎖定,同時(shí)返回成功
- 發(fā)起函數(shù)調(diào)用時(shí),其他線程已經(jīng)鎖定互斥量,或者存在其他線程同時(shí)申請(qǐng)互斥量,但沒有競(jìng)爭(zhēng)到互斥量,那么?pthread_mutex_lock?調(diào)用會(huì)陷入阻塞(執(zhí)行流被掛起),等待互斥量解鎖
改進(jìn)上面 4.1 的例子
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
//定義互斥量,全局,每個(gè)線程都可以看到
pthread_mutex_t mutex;
// 票 -- 共享資源
int tickets = 1000;
void* getTicket(void* args)
{
string username = static_cast<const char*>(args);
while(1)
{
pthread_mutex_lock(&mutex);//加鎖
if(tickets > 0)
{
//模擬搶票花費(fèi)的時(shí)間
usleep(12345);//微秒
cout << username << ": 正在進(jìn)行搶票 " << tickets-- << endl;
pthread_mutex_unlock(&mutex);//解鎖
}
else{
pthread_mutex_unlock(&mutex);//解鎖
break;
}
}
}
int main()
{
pthread_mutex_init(&mutex, nullptr);//初始化互斥量
pthread_t tid1, tid2, tid3, tid4, tid5;
pthread_create(&tid1, nullptr, getTicket, (void*)"thread 1");
pthread_create(&tid2, nullptr, getTicket, (void*)"thread 2");
pthread_create(&tid3, nullptr, getTicket, (void*)"thread 3");
pthread_create(&tid4, nullptr, getTicket, (void*)"thread 4");
pthread_create(&tid5, nullptr, getTicket, (void*)"thread 5");
pthread_join(tid1, nullptr);
pthread_join(tid2, nullptr);
pthread_join(tid3, nullptr);
pthread_join(tid4, nullptr);
pthread_join(tid5, nullptr);
pthread_mutex_destroy(&mutex);//使用完了,銷毀互斥量
return 0;
}
臨界區(qū):
?編譯運(yùn)行,不會(huì)再出現(xiàn)負(fù)數(shù)
我們從運(yùn)行的過程來看:
- 序執(zhí)行變慢了,應(yīng)為有了互斥量,加鎖到解鎖的過程,多個(gè)線程是串行的(同一時(shí)間只能有一個(gè)線程執(zhí)行)
- 但是我們也發(fā)現(xiàn),這些票只有一個(gè)線程在搶(后面需要用線程同步解決)
- 互斥鎖只規(guī)定互斥訪問,沒有規(guī)定必須讓誰先申請(qǐng),誰獲得鎖是多個(gè)執(zhí)行流競(jìng)爭(zhēng)的結(jié)果
搶票后去做一些其他的事:比如生成訂單
編譯運(yùn)行,不會(huì)出現(xiàn)只有一個(gè)線程搶票的情況(后面需要用線程同步解決)
4.4?互斥量實(shí)現(xiàn)原理
如何看待互斥鎖?
- 全局的變量是臨界資源,是需要要被保護(hù)的,鎖是用來保護(hù)臨界資源的
- 鎖是一個(gè)全局資源,所以鎖本身也是一個(gè)臨界資源,鎖需要被保護(hù)么?既然鎖是臨界資源,那么鎖就必須被保護(hù)起來,但鎖本身就是用來保護(hù)臨界資源的,那鎖又由誰來保護(hù)的呢?
- 鎖實(shí)際上是自己保護(hù)自己的,我們只需要保證申請(qǐng)鎖的過程是原子的,那么鎖就是安全的。
- pthread_mutex_lock、pthread_mutex_unlock:加鎖和解鎖的過程其實(shí)就是原子的:如果申請(qǐng)成功,就繼續(xù)向后執(zhí)行,如果申請(qǐng)暫時(shí)沒有成功,執(zhí)行流會(huì)阻塞,誰持有鎖,誰進(jìn)入臨界區(qū)。
在臨界區(qū)內(nèi)執(zhí)行的線程會(huì)進(jìn)行線程切換嗎?
- 臨界區(qū)內(nèi)的線程完全可以進(jìn)行線程切換,但即便該線程被切走,其他線程也無法進(jìn)入臨界區(qū)進(jìn)行資源訪問
- 因?yàn)榇藭r(shí)該線程是拿著鎖被切走的,鎖沒有被釋放也就意味著其他線程無法申請(qǐng)到鎖,也就無法進(jìn)入臨界區(qū)進(jìn)行資源訪問了。
- 其他想進(jìn)入該臨界區(qū)進(jìn)行資源訪問的線程,必須等該線程執(zhí)行完臨界區(qū)的代碼并釋放鎖之后,才能申請(qǐng)鎖,申請(qǐng)到鎖之后才能進(jìn)入臨界區(qū)。
互斥鎖的原子性如何體現(xiàn)?
對(duì)于其他線程而言,有意義的鎖的形態(tài)只有兩種:(1)申請(qǐng)鎖前,(2)申請(qǐng)鎖后。站在其他線程的角度,看待當(dāng)前線程持有鎖的過程,就是原子的?
加鎖、解鎖過程如何保證原子性?? (互斥鎖的原理)
為了實(shí)現(xiàn)互斥鎖操作,大多數(shù)體系結(jié)構(gòu)都提供了 swap 或 exchange 指令,該指令的作用是把寄存器和內(nèi)存單元的數(shù)據(jù)相交換,由于只有一條指令,保證了原子性,即使是多處理器平臺(tái),訪問內(nèi)存的總線周期也有先后,一個(gè)處理器上的交換指令執(zhí)行時(shí)另一個(gè)處理器的交換指令只能等待總線周期。
加鎖和解鎖的偽代碼如下:
%al 是CPU內(nèi)的一個(gè)寄存器(不同體系結(jié)構(gòu)叫法不一樣)?,lock:加鎖(申請(qǐng))的匯編代碼,unlock:解鎖的匯編代碼,都是偽代碼,方便理解,假設(shè)mutex的初始值是1
申請(qǐng)鎖的過程:
- 先把0移動(dòng)到 %al 寄存器里面,即清0
- mutex 變量是我們定義的一個(gè)互斥鎖
- xchgb %al, mutex 就是交換 %al 寄存器和 mutex 中的值,該指令可以完成寄存器和內(nèi)存單元之間數(shù)據(jù)的交換,mutex是存在于內(nèi)存中的(該交換是一條匯編語句完成)
- 然后判斷寄存器內(nèi)的內(nèi)容是否大于0,大于0申請(qǐng)鎖成功,此時(shí)就可以進(jìn)入臨界區(qū)訪問對(duì)應(yīng)的臨界資源。
- 寄存器內(nèi)的內(nèi)容是不大于0,申請(qǐng)失敗,線程被掛起進(jìn)行等待,直到鎖被釋放后再次競(jìng)爭(zhēng)申請(qǐng)鎖
釋放鎖的過程:
- 將內(nèi)存中的mutex的值移動(dòng)為1
- 然后喚醒被掛起的線程,即在等待mutex的線程
申請(qǐng)鎖和釋放鎖的過程不怕被CPU切走,切走回來時(shí)恢復(fù)上下文數(shù)據(jù)即可,所以加鎖和解鎖操作通常是線程安全的
五、可重入和線程安全
5.1?概念
- 線程安全:多個(gè)線程并發(fā)同一段代碼時(shí),不會(huì)出現(xiàn)不同的結(jié)果。常見對(duì)全局變量或者靜態(tài)變量進(jìn)行操作,并且沒有鎖保護(hù)的情況下,會(huì)出現(xiàn)該問題。
- 重入:同一個(gè)函數(shù)被不同的執(zhí)行流調(diào)用,當(dāng)前一個(gè)流程還沒有執(zhí)行完,就有其他的執(zhí)行流再次進(jìn)入,我們稱之為重入。一個(gè)函數(shù)在重入的情況下,運(yùn)行結(jié)果不會(huì)出現(xiàn)任何不同或者任何問題,則該函數(shù)被稱為可重入函數(shù),否則,是不可重入函數(shù)
5.2?常見的線程不安全的情況
- 不保護(hù)共享變量的函數(shù)
- 函數(shù)狀態(tài)隨著被調(diào)用,狀態(tài)發(fā)生變化的函數(shù)
- 返回指向靜態(tài)變量指針的函數(shù)
- 調(diào)用線程不安全函數(shù)的函數(shù)
5.3?常見的線程安全的情況
- 每個(gè)線程對(duì)全局變量或者靜態(tài)變量只有讀取的權(quán)限,而沒有寫入的權(quán)限,一般來說這些線程是安全的
- 類或者接口對(duì)于線程來說都是原子操作
- 多個(gè)線程之間的切換不會(huì)導(dǎo)致該接口的執(zhí)行結(jié)果存在二義性
5.4?常見不可重入的情況
- 調(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)
5.5 常見可重入的情況
- 不使用全局變量或靜態(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ù)
5.6?可重入與線程安全聯(lián)系
- 函數(shù)是可重入的,那就是線程安全的
- 函數(shù)是不可重入的,那就不能由多個(gè)線程使用,有可能引發(fā)線程安全問題
- 如果一個(gè)函數(shù)中有全局變量,那么這個(gè)函數(shù)既不是線程安全也不是可重入的
5.7?可重入與線程安全區(qū)別
- 可重入函數(shù)是線程安全函數(shù)的一種
- 線程安全不一定是可重入的,而可重入函數(shù)則一定是線程安全的。
- 如果將對(duì)臨界資源的訪問加上鎖,則這個(gè)函數(shù)是線程安全的,但如果這個(gè)重入函數(shù)若鎖還未釋放則會(huì)產(chǎn)生死鎖,因此是不可重入的
六、死鎖
6.1 概念
死鎖是指在一組進(jìn)程中的各個(gè)線程均占有不會(huì)被釋放的資源,但因互相申請(qǐng)被其他線程所占用不會(huì)釋放的資源而處于的一種永久等待狀態(tài)
- 在未來,我們可能會(huì)使用多把鎖,假設(shè)線程A持有自己的鎖不釋放,而且還有對(duì)方的鎖,線程B也是如此,線程CDE...,此時(shí)就容易造成死鎖
- 單執(zhí)行流也有可能產(chǎn)生死鎖,如果某一執(zhí)行流連續(xù)申請(qǐng)了兩次鎖(寫的代碼有問題),那么此時(shí)該執(zhí)行流就會(huì)被掛起。因?yàn)樵搱?zhí)行流第一次申請(qǐng)鎖的時(shí)候是申請(qǐng)成功的,但第二次申請(qǐng)鎖時(shí)因?yàn)樵撴i已經(jīng)被申請(qǐng)過了,于是申請(qǐng)失敗導(dǎo)致被掛起直到該鎖被釋放時(shí)才會(huì)被喚醒,但是這個(gè)鎖本來就在自己手上,自己現(xiàn)在處于被掛起的狀態(tài)根本沒有機(jī)會(huì)釋放鎖,所以該執(zhí)行流將永遠(yuǎn)不會(huì)被喚醒,此時(shí)該執(zhí)行流也就處于一種死鎖的狀態(tài)
6.2?死鎖四個(gè)必要條件
- 互斥條件:一個(gè)資源每次只能被一個(gè)執(zhí)行流使用
- 請(qǐng)求與保持條件:一個(gè)執(zhí)行流因請(qǐng)求資源而阻塞時(shí),對(duì)已獲得的資源保持不放
- 不剝奪條件:一個(gè)執(zhí)行流已獲得的資源,在末使用完之前,不能強(qiáng)行剝奪
- 循環(huán)等待條件:若干執(zhí)行流之間形成一種頭尾相接的循環(huán)等待資源的關(guān)系
注:同時(shí)滿足了這四個(gè)條件才會(huì)產(chǎn)生死鎖
6.3?避免死鎖
- 破壞死鎖的四個(gè)必要條件之中的任何一個(gè)條件
- 加鎖順序一致
- 避免鎖未釋放的場(chǎng)景
- 資源一次性分配
除此之外,還有一些避免死鎖的算法,比如死鎖檢測(cè)算法和銀行家算法。
七、Linux線程同步
7.1?同步概念與競(jìng)態(tài)條件
- 同步:在保證數(shù)據(jù)安全的前提下,讓線程能夠按照某種特定的順序訪問臨界資源,從而有效避免饑餓問題,叫做同步
- 競(jìng)態(tài)條件:因?yàn)闀r(shí)序問題,而導(dǎo)致程序異常,稱之為競(jìng)態(tài)條件
同步解釋如下:?
- 如果只是單純的加鎖,是會(huì)存在一些問題的,假設(shè)某個(gè)線程的競(jìng)爭(zhēng)力特別強(qiáng),每次都能夠申請(qǐng)到鎖,但是申請(qǐng)到鎖之后什么也不做,所以在我們看來這個(gè)線程就一直在申請(qǐng)鎖和釋放鎖,這就可能導(dǎo)致其他線程長時(shí)間競(jìng)爭(zhēng)不到鎖,會(huì)引起饑餓問題。
- 單純的加鎖是沒有錯(cuò)的,它能夠保證在同一時(shí)間只有一個(gè)線程進(jìn)入臨界區(qū),但是它不合理,它沒有高效的讓每一個(gè)線程使用這份臨界資源。
- 現(xiàn)在增加一個(gè)規(guī)則,當(dāng)一個(gè)線程釋放鎖后,這個(gè)線程不能立馬再次申請(qǐng)鎖,該線程必須排到這個(gè)鎖的資源等待隊(duì)列的最后進(jìn)行排隊(duì)。
- 增加這個(gè)規(guī)則之后,下一個(gè)獲取到鎖的資源的線程就一定是在資源等待隊(duì)列首部的線程,這就是線程同步
為了支撐線程同步,需要用到條件變量
7.2 條件變量
條件變量的概念:
條件變量是一種同步機(jī)制,用于在多個(gè)線程之間進(jìn)行通信。它允許一個(gè)線程等待另一個(gè)線程滿足特定的條件,然后再繼續(xù)執(zhí)行。條件變量通常與互斥鎖一起使用,以確保線程安全。條件變量提供了一種高效的方式來實(shí)現(xiàn)線程之間的同步和通信。
一個(gè)例子幫助理解條件變量:
- 假設(shè)有一間房間是面試的地方,里面有一位面試官在面試,公司給參見面試的同學(xué)發(fā)送了面試通知,然后一大堆的同學(xué)到這個(gè)面試房間的面前等待。當(dāng)一名同學(xué)面試完成了,面試官準(zhǔn)備面試下一位同學(xué)的時(shí)候,面試官發(fā)現(xiàn)門口都站滿了等待面試的同學(xué)。
- 面試官不知道輪到哪個(gè)了,就隨便叫了離他最近的一名同學(xué)
- 假如那個(gè)叫進(jìn)去的同學(xué)是來得比較晚的,他的并不是前面來的同學(xué),就因?yàn)樗x面試官近,就先進(jìn)去了,這符合規(guī)則么?符合,但是這不合理
- 后面,來個(gè)一個(gè)管理者,對(duì)面試的同學(xué)進(jìn)行管理,管理者直接立起一塊牌子:要面試就先要排隊(duì),只會(huì)從排隊(duì)的里面按順序進(jìn)行面試,不排隊(duì)不能進(jìn)行面試
- 立起來的這個(gè)牌子就相當(dāng)于條件變量,只有符合條件,才允許你進(jìn)行面試。
- 面試的同學(xué)就相當(dāng)于一個(gè)個(gè)的線程,這個(gè)面試的房間就相當(dāng)于一個(gè)公共的資源,即臨界資源,所有進(jìn)程都想訪問這個(gè)資源
- 線程想訪問這個(gè)臨界資源,線程就必須要滿足條件變量,否則線程只能去條件變量下等待
轉(zhuǎn)換成以下可以是:
- 條件變量的內(nèi)部自帶 "排隊(duì)" 的隊(duì)列
- 誰調(diào)用條件變量的等待函數(shù),誰就去排隊(duì)
- 當(dāng)一個(gè)線程收到 “進(jìn)入面試房間的信號(hào)”,它就會(huì)被從隊(duì)列的頭部拿出,允許它訪問使用臨界資源 “面試房間”
7.3 條件變量相關(guān)函數(shù)
初始化條件變量
條件變量跟互斥量一樣是需要初始化的,初始化的函數(shù)是pthread_cond_init,man 3?pthread_cond_init 查看:
函數(shù):pthread_cond_init
頭文件:#include <pthread.h>
函數(shù)原型:
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
參數(shù):
第一個(gè)參數(shù)cond:需要初始化的條件變量
第二個(gè)參數(shù)attr:初始化條件變量的屬性,一般設(shè)置為空即可
返回值:
條件變量初始化成功返回0,失敗返回錯(cuò)誤碼
?調(diào)用pthread_cond_init函數(shù)初始化條件變量叫做動(dòng)態(tài)分配,除此之外,我們還可以用下面這種方式初始化條件變量,該方式叫做靜態(tài)分配:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
銷毀條件變量
銷毀條件變量的函數(shù)叫做pthread_cond_destroy
函數(shù):pthread_cond_destroy
頭文件:#include <pthread.h>
函數(shù)原型:
int pthread_cond_destroy(pthread_cond_t *cond);
參數(shù):
cond:需要銷毀的條件變量
返回值:
條件變量初始化成功返回0,失敗返回錯(cuò)誤碼
注意:使用?PTHREAD_COND_INITIALIZER
初始化的條件變量不需要銷毀
等待條件變量滿足
等待條件變量滿足的函數(shù)叫做pthread_cond_wait?,man 3?pthread_cond_wait 查看:
函數(shù):pthread_cond_wait
頭文件:#include <pthread.h>
函數(shù)原型:
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
參數(shù):
第一個(gè)參數(shù)cond:需要等待的條件變量。
第二個(gè)參數(shù)mutex:當(dāng)前線程所處臨界區(qū)對(duì)應(yīng)的互斥鎖
返回值:
條件變量初始化成功返回0,失敗返回錯(cuò)誤碼
喚醒等待
喚醒等待的函數(shù)有兩個(gè)?pthread_cond_signal 和?pthread_cond_broadcast ,man 3 查看:
函數(shù):pthread_cond_broadcast 和 pthread_cond_signal
頭文件:#include <pthread.h>
函數(shù)原型:
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
參數(shù):
cond:?jiǎn)拘言赾ond條件變量下等待的線程
返回值:
條件變量初始化成功返回0,失敗返回錯(cuò)誤碼
區(qū)別:
- pthread_cond_signal函數(shù)用于喚醒等待隊(duì)列中首個(gè)線程
- pthread_cond_broadcast函數(shù)用于喚醒等待隊(duì)列中的全部線程
使用條件變量的流程:
- 當(dāng)一個(gè)線程需要等待某個(gè)條件時(shí),它會(huì)調(diào)用條件變量的等待函數(shù),并釋放互斥鎖。
- 當(dāng)另一個(gè)線程滿足條件時(shí),它會(huì)發(fā)送信號(hào)通知等待線程,并重新獲取互斥鎖。
- 等待線程接收到信號(hào)后,會(huì)重新獲取互斥鎖并檢查條件是否滿足,如果滿足就繼續(xù)執(zhí)行,否則繼續(xù)等待。
例子,還是上面搶票的例子
主線程創(chuàng)建4個(gè)新線程,讓主線程控制這4個(gè)新線程,這4個(gè)新線程創(chuàng)建后都在條件變量下進(jìn)行等待,直到主線程喚醒一個(gè)等待線程,線程才會(huì)執(zhí)行
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
// 票 -- 共享資源
int tickets = 1000;
//定義全局互斥量 -- 每個(gè)線程都可以看到
pthread_mutex_t mutex;
//定義全局的條件變量
pthread_cond_t cond;
void* getTicket(void* args)
{
string username = static_cast<const char*>(args);
while(1)
{
pthread_mutex_lock(&mutex);//加鎖
pthread_cond_wait(&cond, &mutex);//線程阻塞在這里,直到被喚醒
if(tickets > 0)
{
//注意這里沒有進(jìn)行sleep
cout << username << ": 正在進(jìn)行搶票 " << tickets-- << endl;
pthread_mutex_unlock(&mutex);//解鎖
}
else{
pthread_mutex_unlock(&mutex);//解鎖
break;
}
}
}
int main()
{
pthread_mutex_init(&mutex, nullptr);//初始化互斥量
pthread_cond_init(&cond, nullptr);//初始化條件變量
pthread_t tid1, tid2, tid3, tid4, tid5;
pthread_create(&tid1, nullptr, getTicket, (void*)"thread 1");
pthread_create(&tid2, nullptr, getTicket, (void*)"thread 2");
pthread_create(&tid3, nullptr, getTicket, (void*)"thread 3");
pthread_create(&tid4, nullptr, getTicket, (void*)"thread 4");
while(1)
{
pthread_cond_signal(&cond);//間隔一秒發(fā)送信號(hào),喚醒等待的線程(一個(gè))
cout << "main thread wakeup one thread..." << endl;
sleep(1);
}
pthread_join(tid1, nullptr);
pthread_join(tid2, nullptr);
pthread_join(tid3, nullptr);
pthread_join(tid4, nullptr);
pthread_mutex_destroy(&mutex);//使用完了,銷毀互斥量
pthread_cond_destroy(&cond);//銷毀條件變量
return 0;
}
編譯運(yùn)行
觀察現(xiàn)象會(huì)發(fā)現(xiàn)喚醒這四個(gè)線程時(shí)具有明顯的順序性,根本原因是當(dāng)這若干個(gè)線程啟動(dòng)時(shí)默認(rèn)都會(huì)在該條件變量下去等待,而我們每次都喚醒的是在當(dāng)前條件變量下等待的頭部線程,當(dāng)該線程執(zhí)行完打印操作后會(huì)繼續(xù)排到等待隊(duì)列的尾部進(jìn)行wait,所以我們能夠看到一個(gè)周轉(zhuǎn)的現(xiàn)象
如果我們想每次喚醒都將在該條件變量下等待的所有線程進(jìn)行喚醒,可以將代碼中的pthread_cond_signal
函數(shù)改為pthread_cond_broadcast
函數(shù)
編譯運(yùn)行,每一次喚醒都會(huì)喚醒在該條件變量下等待的所有線程,也就是每次都將這個(gè)線程喚醒
為什么 pthread_cond_wait 的第二個(gè)參數(shù)需要傳入互斥量??
- 因?yàn)樵谡{(diào)用 pthread_cond_wait 函數(shù)時(shí),需要先將互斥量上鎖(加鎖在等待條件變量之前),然后將該互斥量傳遞給函數(shù)作為參數(shù),接著在函數(shù)內(nèi)部會(huì)將該互斥量解鎖并等待條件變量的信號(hào)。
- 如果不傳遞互斥量,就無法保證在等待條件變量時(shí)對(duì)共享資源的訪問是互斥的,可能會(huì)導(dǎo)致數(shù)據(jù)競(jìng)爭(zhēng)等問題,從而可能導(dǎo)致程序錯(cuò)誤或死鎖。
- 因此,為了保證線程之間的安全操作,需要在調(diào)用 pthread_cond_wait 函數(shù)時(shí)傳遞互斥量。
線程互斥與同步完結(jié),下一篇進(jìn)入生產(chǎn)消費(fèi)者模型文章來源:http://www.zghlxwxcb.cn/news/detail-432897.html
--------------------- END ----------------------文章來源地址http://www.zghlxwxcb.cn/news/detail-432897.html
「 作者 」 楓葉先生
「 更新 」 2023.5.3
「 聲明 」 余之才疏學(xué)淺,故所撰文疏漏難免,
或有謬誤或不準(zhǔn)確之處,敬請(qǐng)讀者批評(píng)指正。
到了這里,關(guān)于『Linux』第九講:Linux多線程詳解(三)_ 線程互斥 | 線程同步的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!