多線程編程(二)——互斥鎖與死鎖問題

二元信號量提供了一種很方便的方法來確保對共享變量的互斥訪問,即每個共享變量與一個信號量 s(初始爲1)聯繫起來,然後P(s)和V(s)操作將相應的臨界區包圍起來。互斥鎖是以提供互斥爲目的的二元信號量,二者均屬於掛起等待鎖。

互斥鎖(針對於線程)

互斥鎖用於確保同一時間只有一個線程能訪問被互斥鎖保護的資源(互斥鎖是針對於線程的)。鎖定互斥量的線程與解鎖互斥量的線程必須是同一個線程。

互斥鎖的引入必須對互斥量mutex有一個新的認識,多個線程同時訪問共享數據時可能會發生衝突,這與信號的可重入性是同樣的問題。比如兩個線程要把某個全局變量加1,這個操作在Linux下需要三條指令完成:

(1)從內存讀變量到寄存器;

(2)寄存器值加1;

(3)將寄存器的值寫回內存。

我們在讀取變量的值和把變量的新值保存回去兩步操作之間插入一個 printf 調用,它會執行 write 系統調用進內核,爲內核調用別的線程執行提供一個很好的時機(線程與進程間切換最好的時機:從內核態切換到用戶態)。我們在一個循環中重複上述操作幾千次,就會出現訪問衝突的現象。

#include<stdio.h>
#include<pthread.h>
#include<sys/types.h>
#include<unistd.h>

int count = 0;
void *pthread_run(void* arg)
{
	int val = 0;
	int i = 0;
	while(i < 5000)
	{
		i++;
		val = count;
		printf("pthread:%lu,count:%d\n",pthread_self(),count);
		count = val + 1;
	}
	return NULL;
}
int main()
{
	pthread_t pth1;
	pthread_t pth2;
	pthread_create(&pth1, NULL, &pthread_run, NULL);
	pthread_create(&pth2, NULL, &pthread_run, NULL);
	pthread_join(pth1, NULL);
	pthread_join(pth2, NULL);
	printf("count:%d\n",count);
	return 0;
}
運行結果如下圖所示:


上述程序的運行結果足以說明一切,正確結果應該是10000,程序在用戶態與內核態之間不斷的切換,兩個線程對其進行訪問(因爲i++不是原子的),則必然導致不可預料的結果。

對於多線程的訪問,訪問衝突的問題是非常普遍的,解決的方法就是加入互斥鎖,獲得鎖的線程可以完成“讀-修改-寫”的操作,然後釋放了鎖給其他線程,沒有獲得鎖的線程只能等待而不能訪問共享數據,這樣“讀-修改-寫”三步操作組成了一個原子操作

如果mutex_enter不能設置鎖(因爲另外一個進程已經設置了),則阻塞動作將取決於互斥對象中保存的專用類型信息。與互斥鎖相關聯的操作如下所示:

int pthread_mutex_init(pthread_mutex_t * mutex , pthread_mutexattr_t * attr);
int pthread_mutex_destroy (pthread_mutex_t * mutex);
int pthread_mutex_lock (pthread_mutex_t * mutex );
int pthread_mutex_unlock (pthread_mutex_t * mutex );
int pthread_mutex_trylock (pthread_mutex_t * mutex );

(1)pthread_mutex_init:初始化互斥鎖

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

參數mutex:互斥鎖地址,類型爲 pthread_mutex_t;

參數attr:設置互斥量的屬性,通常採用默認屬性設置爲NULL;

如果mutex變量是靜態分配的(全局變量或static變量),也可以用宏定義PTHREAD_MUTEX_INITIALIZER來初始化,相當於pthread_mutex_init初始化並且attr設置爲NULL。

返回值:成功返回0,失敗返回錯誤碼。

(2)pthread_mutex_destroy:銷燬互斥鎖

int pthread_mutex_destroy(pthread_mutex_t *mutex);

直接加互斥鎖的地址進行銷燬。

(3)pthread_mutex_lock:加鎖,掛起等待鎖(與二元信號量一樣)

int pthread_mutex_lock(pthread_mutex_t *mutex);

參數mutex:互斥鎖的地址;

返回值:成功返回0,失敗返回錯誤碼。

一個線程可以調用此函數來對mutex加鎖,如果另外一個線程想獲得mutex,則只能掛起等待,直到當前線程釋放鎖爲止。

掛起等待在嚴格意義上是:每個mutex有一個等待隊列,一個線程在mutex上等待掛起,首先要將自己的加入到等待隊列裏,本質上是將PCB加入等待隊列中。

(4)pthread_mutex_unlock:解鎖

int pthread_mutex_unlock(pthread_mutex_t *mutex);

參數mutex:互斥鎖地址;

返回值:成功返回0,失敗返回錯誤碼。

使用此函數釋放mutex,當前線程將被喚醒,才能獲得該mutex並繼續執行。

(5)pthread_mutex_trylock:加鎖,嘗試失敗後立即返回,不等待的非阻塞式

int pthread_mutex_trylock(pthread_mutex_t *mutex);

如果一個線程既想得到鎖,又不想掛起等待,可以調用 trylock,如果mutex已經被另外一個線程獲得,這個函數會失敗返回EBUSY,而不會使線程掛起等待。

互斥鎖測試

注意:pthread不是Linux下的默認的庫,也就是說在鏈接的時候無法找到pthread庫中各個函數的入口地址,於是會鏈接失敗,所以在編譯的時候要加上 -lpthread 參數。

#include<stdio.h>
#include<pthread.h>
#include<sys/types.h>
#include<unistd.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

int count = 0;
void *pthread_run(void* arg)
{
	int val = 0;
	int i = 0;
	while(i < 5000)
	{
		//對臨界區加鎖
		pthread_mutex_lock(&mutex);
		i++;
		val = count;
		printf("pthread:%lu,count:%d\n",pthread_self(),count);
		count = val + 1;
		//解鎖操作
		pthread_mutex_unlock(&mutex);
	}
	return NULL;
}
int main()
{
	pthread_t pth1;
	pthread_t pth2;
	pthread_create(&pth1, NULL, &pthread_run, NULL);
	pthread_create(&pth2, NULL, &pthread_run, NULL);
	pthread_join(pth1, NULL);
	pthread_join(pth2, NULL);
	printf("count:%d\n",count);
	return 0;
}
運行結果如下圖所示:

當我們對臨界區加鎖之後,程序的運行結果符合我們的預期,多個線程訪問臨界資源不會再發生衝突。

死鎖的原理

信號量潛在的引入了令人厭惡的運行時錯誤,稱之爲死鎖(deadlock),它指的是一組線程被阻塞了,等待一個永遠也不會爲真的條件,它是一組相互競爭系統資源或進程通信的進程間的“永久”阻塞。

下面展示了一對用兩個信號量來實現互斥的線程的進度圖:

線程1申請到 s,線程2申請到 t,由於繼續執行時,線程1阻塞在 t 上,而線程2阻塞在 s 上,因而形成了死鎖。

(1)使用PV操作順序不當,以至於兩個信號量的禁止區域重疊。如果某個執行軌線恰巧到達死鎖區域d,則不可能繼續發展。也驗證了死鎖是因爲每個線程都在等待其他線程執行一個根本不可能發生的V操作。

(2)重疊的禁止區域引入了一組稱爲死鎖區域(deadlock regin)的狀態。軌跡線一旦進入死鎖區域,那麼死鎖則必然發生。

互斥鎖加鎖順序規則:如果對於程序中每對互斥鎖(s,t),給出所有的鎖分配的一個詳細使用銓敘,每個線程按照這個順序來申請鎖,並且按照逆序來釋放,那麼這個程序就是無死鎖的。

死鎖的條件

死鎖的四個必要條件:

(1)互斥,一次只有一個進程可以使用一個資源,其他進程不能訪問已分配給其他進程的資源。

(2)佔有且等待,當一個進程等待其他進程時,繼續佔有已經分配的資源。

(3)不可搶佔,不能強行搶佔進程已佔有的資源。

(4)循環等待,存在一個封閉的進程鏈,使得每個進程至少佔有此鏈中下一個進程所需要的一個資源。

死鎖預防

(1)破壞互斥條件:一般來說,這個條件是不可能禁止的,如果需要對資源進行互斥訪問,那麼操作系統必須支持互斥。

(2)資源一次性分配:可以要求進程一次性請求所有需要的資源,並且阻塞這個進程直到所有請求同時滿足。

但是存在三個問題

①  一個進程可能被阻塞很長時間以等待所有資源,但其他進程可能只需要一部分資源就可以運行。

② 分配給一個進程的資源可能有很長一段時間沒有被使用,在此期間,也不會被其他進程使用。

③ 一個進程可能事先不知道他說需要的全部資源。

(3)破壞不可搶佔條件:但是這種只能是資源狀態可以很容易保存和恢復的前提下才是實用的。

(4)資源的有序分配:對資源按照序號進行申請鎖,確保鎖的分配順序可以預防死鎖。但是它可能是低效的,它會使進程執行速度變慢,並且可能在沒有必要的情況下拒絕資源訪問。

避免死鎖的兩個方法:

(1)如果一個進程的請求會導致死鎖,則不啓動此進程。

(2)如果一個進程增加的資源請求會導致死鎖,則不允許此分配。

銀行家算法:資源分配拒絕策略,又稱爲銀行家算法,首次提出了安全狀態,即至少有一個資源分配序列不會導致死鎖(即所有進程都能運行直到結束)。

講到這裏死鎖問題也就闡述完結,不知道大家是否對死鎖有了新的認識,感興趣的可以編寫一下銀行家算法,當然還有重要的哲學家就餐問題,這裏不再做詳細闡述。


發佈了78 篇原創文章 · 獲贊 98 · 訪問量 19萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章