一、線程互斥
1.1互斥概念的引入
先來(lái)用代碼模擬一個(gè)搶票的場(chǎng)景,四個(gè)線程不停地?fù)屍保还灿?000張票,搶完為止,代碼如下:
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <memory>
int tickets=1000;
void* get_ticket(void* args)
{
std::string username=static_cast<const char*>(args);
while(true)
{
if(tickets>0)
{
usleep(1000);//用來(lái)模擬搶票花費(fèi)時(shí)間
std::cout<<username<<" 搶票ing"<<tickets<<std::endl;
tickets--;
}
else
{
break;
}
}
return nullptr;
}
int main()
{
pthread_t t1,t2,t3,t4;
pthread_create(&t1,nullptr,get_ticket,(void*)"線程1");
pthread_create(&t2,nullptr,get_ticket,(void*)"線程2");
pthread_create(&t3,nullptr,get_ticket,(void*)"線程3");
pthread_create(&t4,nullptr,get_ticket,(void*)"線程4");
pthread_join(t1,nullptr);
pthread_join(t2,nullptr);
pthread_join(t3,nullptr);
pthread_join(t4,nullptr);
return 0;
}
執(zhí)行結(jié)果如下:
線程2 搶票ing線程4 搶票ing4
線程3 搶票ing2
4
線程1 搶票ing0
線程4 搶票ing-1
線程3 搶票ing-2
[sny@VM-8-12-centos threaddone]$
可以看到,最后出現(xiàn)了票數(shù)為負(fù)數(shù)的情況,很顯然這是錯(cuò)誤的,是不應(yīng)該出現(xiàn)的。
為什么會(huì)出現(xiàn)這種情況?
首先要明確,上述的幾個(gè)線程是不能同時(shí)執(zhí)行搶票的動(dòng)作的。
但是,這幾個(gè)線程可以在執(zhí)行的過(guò)程中不斷地切換,即當(dāng)一個(gè)線程還沒(méi)有執(zhí)行完搶票的動(dòng)作的時(shí)候,就可以被另一個(gè)線程切走。
而眾所周知,進(jìn)行計(jì)算操作時(shí),數(shù)據(jù)要被加載到CPU中進(jìn)行運(yùn)算,之后再寫回內(nèi)存中,并且CPU中的寄存器只有一套,每一個(gè)線程離開CPU時(shí),要將寄存器中的屬于自己的上下文帶走,到下一次執(zhí)行時(shí)再將上下文數(shù)據(jù)寫回CPU中進(jìn)行沒(méi)有完成的操作。
所以,當(dāng)一個(gè)線程準(zhǔn)備搶票時(shí),卻突然被另一個(gè)線程切走。這時(shí),該線程的上下文記錄中,tickets是大于0的,但是很有可能另一個(gè)線程已經(jīng)把票搶完了。所以,該線程再一次運(yùn)行時(shí),就會(huì)誤以為tickets>0,再次搶票,就出現(xiàn)了票數(shù)為負(fù)數(shù)的情況。
所以,當(dāng)我們定義全局變量,且有多個(gè)線程執(zhí)行時(shí),該變量很有可能是不安全的。
這里再補(bǔ)充幾個(gè)概念:
- 多個(gè)執(zhí)行流進(jìn)行安全訪問(wèn)的共享資源叫做臨界資源
- 多執(zhí)行流中,訪問(wèn)臨界資源的代碼叫做臨界區(qū)(往往是線程代碼中很小的一部分)
- 讓多個(gè)線程串行訪問(wèn)共享資源叫做互斥
- 對(duì)一個(gè)資源進(jìn)行訪問(wèn)時(shí),要么不做,要么做完(原子性----一個(gè)對(duì)資源進(jìn)行的操作,如果只用一條匯編代碼就能完成,就稱它為原子的)
所以,為了能保證共享資源的安全性,就要進(jìn)行一個(gè)加鎖的操作:
對(duì)資源訪問(wèn)結(jié)束后,再進(jìn)行解鎖操作。
1.2詳解互斥量
1.2.1對(duì)樣例代碼加鎖
先二話不說(shuō),用鎖對(duì)上面的代碼中的臨界資源做保護(hù),隨后再解釋原理,代碼如下:
int tickets=1000;
class ThreadData
{
public:
ThreadData(const std::string threadname,pthread_mutex_t* mutex_P)
:threadname_(threadname)
,mutex_p_(mutex_P)
{}
~ThreadData()
{}
public:
std::string threadname_;
pthread_mutex_t* mutex_p_;
};
void* get_ticket(void* args)
{
ThreadData* td=static_cast<ThreadData*>(args);
while(true)
{
pthread_mutex_lock(td->mutex_p_);//加鎖
if(tickets>0)
{
usleep(1000);//用來(lái)模擬搶票花費(fèi)時(shí)間
std::cout<< td->threadname_<<" 搶票ing"<<tickets<<std::endl;
tickets--;
pthread_mutex_unlock(td->mutex_p_);//解鎖
}
else
{
pthread_mutex_unlock(td->mutex_p_);
break;
}
}
return nullptr;
}
int main()
{
#define NUM 4
pthread_mutex_t lock;
pthread_mutex_init(&lock,nullptr);//初始化鎖
std::vector<pthread_t> tids(NUM);
for(int i=0;i<NUM;i++)
{
char buffer[64];
snprintf(buffer,sizeof(buffer),"thread %d",i+1);
ThreadData* td=new ThreadData(buffer,&lock);
pthread_create(&tids[i],nullptr,get_ticket,td);
}
for(const auto& tid:tids)
{
pthread_join(tid,nullptr);
}
pthread_mutex_destroy(&lock);//銷毀鎖
return 0;
}
運(yùn)行結(jié)果如下:
thread 4 搶票ing4
thread 4 搶票ing3
thread 4 搶票ing2
thread 4 搶票ing1
[sny@VM-8-12-centos threaddone]$
可以看到,這次搶票的結(jié)果沒(méi)有出現(xiàn)負(fù)數(shù)的情況,但是,這次只有線程4在搶票,這是因?yàn)殒i只規(guī)定互斥訪問(wèn),沒(méi)有規(guī)定誰(shuí)優(yōu)先執(zhí)行,鎖就是讓多個(gè)執(zhí)行流進(jìn)行競(jìng)爭(zhēng)的結(jié)果。而且,由于加鎖之后,所有線程是串行的,所以這次運(yùn)行速度會(huì)慢一些。
當(dāng)然,搶票結(jié)束之后還要讓每一個(gè)執(zhí)行流去做自己的事,這樣其他執(zhí)行流就也可以搶到票了,讓每個(gè)線程搶完票之后usleep一段時(shí)間用來(lái)模擬實(shí)現(xiàn)其他業(yè)務(wù)。
1.2.2如何看待鎖?
上文中定義的tickets是一個(gè)全局變量,為保護(hù)全局變量就要加鎖。但是每一個(gè)線程訪問(wèn)全局變量之前都要訪問(wèn)鎖,所以鎖本身就是一個(gè)全局變量,那鎖的安全怎么保護(hù)?
加鎖的過(guò)程是原子的,所以加鎖和解鎖的過(guò)程是十分安全的。
如果一個(gè)執(zhí)行流申請(qǐng)鎖失敗怎么辦?----答案是執(zhí)行流會(huì)阻塞,直到它被喚醒。
舉個(gè)例子:
while(true)
{
pthread_mutex_lock(td->mutex_p_);//加鎖
pthread_mutex_lock(td->mutex_p_);//加鎖
if(tickets>0)
{
usleep(1000);//用來(lái)模擬搶票花費(fèi)時(shí)間
std::cout<< td->threadname_<<" 搶票ing"<<tickets<<std::endl;
tickets--;
pthread_mutex_unlock(td->mutex_p_);//解鎖
}
else
{
pthread_mutex_unlock(td->mutex_p_);
break;
}
}
如上,對(duì)每一個(gè)線程申請(qǐng)鎖成功之后再一次申請(qǐng),就必定會(huì)失敗。
運(yùn)行結(jié)果如下:
可以看到,四個(gè)線程是存在的,但是它們都處于阻塞狀態(tài)。直到該線程現(xiàn)在持有的鎖釋放,操作系統(tǒng)才會(huì)喚醒該線程,讓它繼續(xù)持有下一個(gè)鎖并向后執(zhí)行。
也可以使用pthread_mutex_trylock
這個(gè)接口可以判斷當(dāng)前線程有沒(méi)有持有鎖,沒(méi)有就申請(qǐng)一個(gè)鎖并返回,有就直接返回,不會(huì)造成阻塞的問(wèn)題。
根據(jù)以上內(nèi)容,不難判斷,只有持有鎖的線程才可以訪問(wèn)臨界區(qū)!
還要注意幾點(diǎn):
- 如果一個(gè)線程申請(qǐng)鎖成功并且正在訪問(wèn)臨界資源,其他線程會(huì)處于阻塞狀態(tài)
- 如果一個(gè)線程申請(qǐng)鎖成功并且正在訪問(wèn)臨界資源,該線程可以被其他線程切換
- 當(dāng)持有鎖的線程被切走,其他線程依舊無(wú)法申請(qǐng)鎖,也就無(wú)法訪問(wèn)臨界資源,不能向后執(zhí)行,直到持有鎖的線程釋放鎖
- 對(duì)于一個(gè)線程,有意義的狀態(tài)只有持有鎖和釋放鎖兩種狀態(tài)。站在其他線程角度,該線程持有鎖的過(guò)程就是原子的
1.2.3如何理解加鎖的本質(zhì)
上文中說(shuō)了加鎖和解鎖是原子性的,那么這個(gè)過(guò)程是怎么實(shí)現(xiàn)原子性的呢?
當(dāng)我們對(duì)匯編代碼稍有了解之后,就會(huì)知道即便是非常簡(jiǎn)單的諸如i++/i–這樣的運(yùn)算,都不可能只用一條匯編代碼就能完成。對(duì)于這種不能“瞬間”完成的非原子性的運(yùn)算,在多線程環(huán)境下,很可能被其他線程中斷并修改數(shù)據(jù),導(dǎo)致數(shù)據(jù)錯(cuò)誤。
為了實(shí)現(xiàn)互斥鎖操作,大多數(shù)體系結(jié)構(gòu)都提供了swap或exchange指令,該指令的作用是把寄存器和內(nèi)存單元的數(shù)據(jù)想交換,由于只有一條指令,保證了原子性,即使是多處理器平臺(tái),訪問(wèn)內(nèi)存的總線周期也有先后,一個(gè)處理器上的交換指令執(zhí)行時(shí),另一個(gè)處理器的交換指令只能等待總線周期。
以上便可實(shí)現(xiàn)加鎖和解鎖的原子性。
三、對(duì)鎖進(jìn)行封裝
封裝過(guò)程比較簡(jiǎn)單,直接上代碼:
#pragma once
#include <iostream>
#include <pthread.h>
class Mutex
{
public:
Mutex(pthread_mutex_t* lock_P=nullptr):lock_p_(lock_P)
{}
void lock()
{
if(lock_p_) pthread_mutex_lock(lock_p_);
}
void unlock()
{
if(lock_p_) pthread_mutex_unlock(lock_p_);
}
~Mutex()
{}
private:
pthread_mutex_t* lock_p_;
};
class LockGuard
{
public:
LockGuard(pthread_mutex_t* mutex) :mutex_(mutex)
{
mutex_.lock();//在構(gòu)造函數(shù)中加鎖
}
~LockGuard()
{
mutex_.unlock();//在析構(gòu)函數(shù)中解鎖
}
private:
Mutex mutex_;
};
加鎖時(shí),就只需要?jiǎng)?chuàng)建一個(gè)鎖變量,并將其傳遞給LockGuard對(duì)象即可自動(dòng)初始化,是用完之后會(huì)自動(dòng)銷毀。使用比較簡(jiǎn)單,這里就不演示了。
四、可重入和線程安全
4.1概念
- 線程安全:多個(gè)線程并發(fā)同一段代碼時(shí),不會(huì)出現(xiàn)不同的結(jié)果。常見對(duì)全局變量或者靜態(tài)變量進(jìn)行操作,并且沒(méi)有鎖保護(hù)的情況下,會(huì)出現(xiàn)該問(wèn)題。
- 重入:同一個(gè)函數(shù)被不同的執(zhí)行流調(diào)用,當(dāng)前一個(gè)流程還沒(méi)有執(zhí)行完,就有其他的執(zhí)行流再次進(jìn)入,我們稱之為重入。一個(gè)函數(shù)在重入的情況下,運(yùn)行結(jié)果不會(huì)出現(xiàn)任何不同或者任何問(wèn)題,則該函數(shù)被稱為可重入函數(shù),否則,是不可重入函數(shù)。
4.2常見線程安全和不安全的情況
常見線程不安全的情況:
- 不保護(hù)共享變量的函數(shù)
- 函數(shù)狀態(tài)隨著被調(diào)用,狀態(tài)發(fā)生變化的函數(shù)
- 返回指向靜態(tài)變量指針的函數(shù)
- 調(diào)用線程不安全函數(shù)的函數(shù)
常見線程安全的情況:
- 每個(gè)線程對(duì)全局變量或者靜態(tài)變量只有讀取的權(quán)限,而沒(méi)有寫入的權(quán)限,一般來(lái)說(shuō)這些線程是安全的
- 類或者接口對(duì)于線程來(lái)說(shuō)都是原子操作
- 多個(gè)線程之間的切換不會(huì)導(dǎo)致該接口的執(zhí)行結(jié)果存在二義性
4.3常見可重入和不可重入的情況
常見不可重入的情況:
- 調(diào)用了malloc/free函數(shù),因?yàn)閙alloc函數(shù)是用全局鏈表來(lái)管理堆的
- 調(diào)用了標(biāo)準(zhǔn)I/O庫(kù)函數(shù),標(biāo)準(zhǔn)I/O庫(kù)的很多實(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ù),或者通過(guò)制作全局?jǐn)?shù)據(jù)的本地拷貝來(lái)保護(hù)全局?jǐn)?shù)據(jù)
4.4可重入與線程安全的聯(lián)系
- 函數(shù)是可重入的,那就是線程安全的
- 函數(shù)是不可重入的,那就不能由多個(gè)線程使用,有可能引發(fā)線程安全問(wèn)題
- 如果一個(gè)函數(shù)中有全局變量,那么這個(gè)函數(shù)既不是線程安全也不是可重入的。
4.5可重入和線程安全的區(qū)別
- 可重入函數(shù)是線程安全函數(shù)的一種
- 線程安全不一定是可重入的,而可重入函數(shù)則一定是線程安全的。
- 如果將對(duì)臨界資源的訪問(wèn)加上鎖,則這個(gè)函數(shù)是線程安全的,但如果這個(gè)重入函數(shù)若鎖還未釋放則會(huì)產(chǎn)生死鎖,因此是不可重入的。
五、死鎖
死鎖是指在一組進(jìn)程中的各個(gè)進(jìn)程均占有不會(huì)釋放的資源,但因互相申請(qǐng)被其他進(jìn)程所站用不會(huì)釋放的資源而處于的一種永久等待狀態(tài)。
用大白話來(lái)理解上面這句話就是,在多把鎖的場(chǎng)景下,每一個(gè)執(zhí)行流都持有自己的鎖,在不釋放的情況下,還想申請(qǐng)其他執(zhí)行流的鎖,其他的執(zhí)行流也是這樣的情況,這時(shí)就造成了死鎖的情況。
形成死鎖的四個(gè)必要條件:
①互斥:一個(gè)資源每次只能被一個(gè)執(zhí)行流使用
②請(qǐng)求與保持:一個(gè)執(zhí)行流因請(qǐng)求資源而阻塞時(shí),對(duì)已獲得的資源保持不放
③不剝奪:一個(gè)執(zhí)行流已獲得的資源,在末使用完之前,不能強(qiáng)行剝奪
④循環(huán)等待:若干執(zhí)行流之間形成一種頭尾相接的循環(huán)等待資源的關(guān)系
怎么解決死鎖?
上述四個(gè)是形成死鎖的必要條件,所以解決死鎖只需要破壞上述任意一個(gè)條件即可。
首先,申請(qǐng)鎖一定是為了互斥訪問(wèn)資源,所以這個(gè)條件一定是成立的,無(wú)法破壞。
其次,對(duì)于第二個(gè)條件。如果一個(gè)執(zhí)行流已經(jīng)申請(qǐng)到了鎖,還想申請(qǐng)下一個(gè),可以采用一定策略使得這次申請(qǐng)失敗,或者釋放原來(lái)的鎖,再申請(qǐng)下一個(gè)鎖,如此便可解決請(qǐng)求與保持的問(wèn)題。
再次,當(dāng)多個(gè)執(zhí)行流不能剝奪其他執(zhí)行流的鎖時(shí),我們可以設(shè)置一個(gè)比較策略,比較出在某一方面較差的一個(gè)執(zhí)行流,讓其主動(dòng)交出自己持有的鎖即可。
最后,可以通過(guò)控制線程申請(qǐng)鎖的順序,來(lái)避免環(huán)路等待的問(wèn)題。
常見的兩種解決死鎖的算法:
①死鎖檢測(cè)算法
②銀行家算法
看興趣的讀者可以通過(guò)這兩個(gè)鏈接了解一下。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-403206.html
本篇完,下一篇為【關(guān)于Linux中----線程同步】,敬請(qǐng)期待!文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-403206.html
到了這里,關(guān)于【關(guān)于Linux中----線程互斥與同步】的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!