在多線程編程中,由於線程間共享地址空間與大部分資源,而當多個線程同時訪問共享數據的時候,就很有可能會發生衝突導致錯誤。
如下對於一個共享的全局變量進行累加,線程1與線程2均分別做累加5000次操作:
#include<stdio.h>
#include<pthread.h>
int g_a=0;
void* funtest(void* arg)
{
int i=0;
for(;i<5000;i++)
{
int tmp=g_a;
printf("the ptheard tid is %u,the g_a is %d\n",pthread_self(),g_a);
g_a=tmp+1;
}
}
int main()
{
pthread_t tid1,tid2;
int err=pthread_create(&tid1,NULL,funtest,NULL);
if(err!=0)
{
printf("pthread_create:%s\n",strerror(err));
return -1;
}
err=pthread_create(&tid2,NULL,funtest,NULL);
if(err!=0)
{
printf("pthread_create:%s\n",strerror(err));
return -1;
}
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
printf("at last,g_a=%d\n",g_a);
return 0;
}
其實,按理來說我們想的是讓線程1和線程2共同對g_a進行累加5000次的操作,預想的結果應該是最後g_a的值爲10000,然而,
很明顯g_a的值不是10000,經過多次測試,g_a的值會變,但均在5000左右。
而出現這種現象的原因在於g_a這個變量是屬於線程1和線程2所共享的,而對於g_a的累加5000次的操作都是相同的,在線程1對g_a進行累加的時候,很有可能線程2也對g_a進行了操作,而且它們的累加操作並不是原子操作,讀取g_a的值,以及修改g_a的值很有可能在被線程1和線程2的共同影響下被打亂。
而對於這種對共享資源的訪問衝突,我們可以通過互斥鎖來進行解決,利用互斥鎖將對共享資源的所有操作進行保護,使得同一時間只能有一個線程能對申請到我們的共享資源,而此時其他線程無法申請到我們的共享資源。
對於使用互斥鎖,首先得創建一個互斥鎖類型pthread_mutex_t的變量;
然後通過以下系統調用,進行操作:
①int pthread_mutex_init(pthread_mutex_t* mutex,const pthread_mutexattr_t* attr);
函數功能:對互斥鎖進行初始化
參數:第一個參數表示一個指向互斥鎖類型的指針,第二個參數表示互斥鎖的屬性,爲NULL時表示缺省屬性
返回值:若成功返回0,否則返回錯誤號
注意:若已定義的互斥鎖變量爲全局變量或者是靜態變量,可以用PTHREAD_MUTEX_INITIALIZER這個宏來進行初始化(與pthread_mutex_init函數,第二個參數爲NULL時作用相同)
②int pthread_mutex_destory(pthread_mutex_t* mutex);
函數功能:對互斥鎖變量進行釋放
參數:唯一參數表示一個指向互斥鎖類型的指針
返回值:若成功返回0,否則返回錯誤號
③int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_trylock(pthread_mutex_t* mutex);
函數功能:兩個函數均是執行加鎖操作,前者當這個鎖已經被其他線程所佔用時,當前線程掛起等待,直到佔用鎖的線程退出,阻塞式;而後者則是不會掛起等待,而是返回EBUSY,非阻塞式
④int pthread_mutex_unlock(pthread_mutex_t* mutex);
函數功能:執行解鎖操作,對於鎖進行釋放,可供其他線程申請使用
返回值:若成功則返回0,否則返回錯誤碼
對最開始的測試用例進行修改:
#include<stdio.h>
#include<pthread.h>
int g_a=0;
pthread_mutex_t Mutex=PTHREAD_MUTEX_INITIALIZER;
void* funtest(void* arg)
{
int i=0;
for(;i<5000;i++)
{
pthread_mutex_lock(&Mutex);
int tmp=g_a;
printf("the ptheard tid is %u,the g_a is %d\n",pthread_self(),g_a);
g_a=tmp+1;
pthread_mutex_unlock(&Mutex);
}
}
int main()
{
pthread_t tid1,tid2;
// pthread_mutex_init(&Mutex,NULL);
int err=pthread_create(&tid1,NULL,funtest,NULL);
if(err!=0)
{
printf("pthread_create:%s\n",strerror(err));
return -1;
}
err=pthread_create(&tid2,NULL,funtest,NULL);
if(err!=0)
{
printf("pthread_create:%s\n",strerror(err));
return -1;
}
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
pthread_mutex_destory(&Mutex);
printf("at last,g_a=%d\n",g_a);
return 0;
}
測試結果:
很明顯達到預期想法。
下面對於互斥鎖進行討論一下,我們的互斥鎖變量在創建時,很明顯它是屬於兩個線程共享資源,所以對於互斥鎖,想要保護我們的共享資源,首先要保護好自己,因此對於互斥鎖的加鎖與解鎖必須保證他的操作的原子性。
對於解鎖操作,無非是對一個內存單元進行賦值(由0變1),這個操作顯然是原子操作。
但對於加鎖操作,若是僅通過對於互斥鎖變量與0進行比較,若大於0的話對變量進行賦值修改爲0這樣的操作的話,我們可以發現這個操作是將內存中的數據讀至CPU當中,然後才進行比較,這樣的操作顯然不是原子操作,況且就算在比較的時候沒有切換線程,只要是在對mutex賦值爲0操作之前,線程被切出去了,那其他線程依舊可以申請到鎖,訪問臨界資源
所以爲了實現原子操作,可以通過下面的操作:
利用一個寄存器,將其初始化爲0,利用exchange或swap指令,將我們內存中的互斥鎖變量與寄存器的值進行交換,由於線程在切換時會保存上下文信息,而我們的寄存器信息就是需要被保存的一部分,所以無論線程間如何切換,寄存器的信息是不會改變的,因此下面的每一步操作都是原子操作,而且就算在期間線程切換出去,只要有人把鎖申請了,mutex就會一直爲0,滿足我們只能讓一個線程申請到鎖,能夠訪問到臨界資源的要求。
以上我們大致明白了互斥鎖的原理,但是互斥鎖在使用的時候,存在一個嚴重的問題,也就是常常提到的死鎖問題。
1.首先什麼是死鎖?
從很多別人寫的博客中,關於死鎖,大致都是這麼說的:線程在競爭申請一份公共資源的情況下,互相等待對方讓步,而導致一直等待的問題就是死鎖。
與其看這些概念,我們不如來看看死鎖的兩種典型情形:
①單個線程的死鎖:如果一個線程對於同一把鎖連續進行了兩次申請,即兩次lock,在第一次申請鎖之後還沒有進行解鎖,又第二次進行申請鎖,由於第一次申請鎖之後還沒有進行解鎖,在第二次申請的時候,鎖被佔用着不能被再次申請,因此該線程掛起等待鎖的釋放,一旦掛起,那麼就不可能等到解鎖的時候,所以線程一直處於掛起,造成死鎖
②多個線程的死鎖:如果線程1申請到了A鎖,線程2申請到了B鎖,而在線程1對A鎖進行解鎖之前,又再次申請B鎖,而線程2又在對B鎖解鎖之前,申請了A鎖,那麼線程1在等待線程2釋放B鎖,掛起等待,線程2在等待線程1釋放A鎖,掛起等待,兩者在互相等待,就會一直處於掛起狀態,造成死鎖
由此,我們可以得出產生死鎖的原因主要是競爭相同的資源,進程(線程)的推進順序不當
2.造成死鎖的必要條件
①互斥條件:線程要求對所分配的資源進行排它性控制,即在一段時間內某資源僅爲一線程所佔用
②請求和保持條件:當線程因請求資源而阻塞時,對已獲得的資源保持不放
③不剝奪條件:線程已獲得的資源在未使用完之前,不能剝奪,只能在使用完時由自己釋放
④環路等待條件:在發生死鎖時,必然存在一個線程--資源的環形鏈,即進程集合{P0,P1,P2…Pn}中的P0正在等待一個P1佔用的資源,P1正在等待P2佔用的資源,……,Pn正在等待已被P0佔用的資源
3.如何避免死鎖
①預防死鎖
資源一次性分配:對於線程請求的資源,一次性給它進行分配(破壞請求和保持條件)
可剝奪資源:當線程的新申請的資源未能滿足時可以對已佔有的資源進行釋放(破壞不剝奪條件)
資源有序分配法:對於進程的資源進行編號,線程在申請時必須按照編號順序進行申請(破壞環路等待條件)
但是對於上述的方法,雖能預防死鎖,但是不可避免的造成對資源分配的嚴格限制,造成系統性能的下降
②避免死鎖
允許線程動態的申請資源,最典型的算法就是銀行家算法:
對於系統而言,在分配資源的之前,預先計算資源分配的安全性,若是資源分配安全,那麼就分配對應資源,否則讓我們的線程進行等待
(1) 當一個進程對資源的最大需求量不超過系統中的資源數時可以接納該進程。
(2) 進程可以分期請求資源,當請求的總數不能超過最大需求量。
(3) 當系統現有的資源不能滿足進程尚需資源數時,對進程的請求可以推遲分配,但總能使進程在有限的時間裏得到資源。
(4) 當系統現有的資源能滿足進程尚需資源數時,必須測試系統現存的資源能否滿足該進程尚需的最大資源數,若能滿足則按當前的申請量分配資源,否則也要推遲分配。
③檢測死鎖
通過系統所設置的檢測機制,及時地檢測出死鎖的發生,並精確地確定與死鎖有關的進程和資源,然後採取適當措施,從系統中將已發生的死鎖清除掉
④解除死鎖
這是與檢測死鎖相配套的一種措施。當檢測到系統中已發生死鎖時,須將進程從死鎖狀態中解脫出來。常用的實施方法是撤銷或掛起一些進程,以便回收一些資源,再將這些資
源分配給已處於阻塞狀態的進程,使之轉爲就緒狀態,以繼續運行