Linux--線程死鎖

線程爲什會死鎖??“鎖”又是什麼東西?我們這篇博客主要講一下爲什麼要給線程加鎖,爲什麼會出現線程死鎖,線程死鎖怎麼解決。

互斥鎖

在我的上篇博客已經講解了一些線程的基本知識Linux–線程控制我們可以瞭解到線程是共享同一份內存的。這就意味着多個線程同時訪問共享數據時可能發生衝突。

分析程序


我們首先分析一個小程序:

#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>

static int g_count = 0;
void* read_write_num(void* _val)
{
    int val = 0;
    int i = 0;
    for(; i<5000; ++i)
    {
        val  = g_count;
        printf("pthread id is:%x,count is : %d\n",(unsigned long)pthread_self(),g_count);
        g_count = val + 1;
    }
    return NULL;
}

int main()
{
    pthread_t tid1;
    pthread_t tid2;
    pthread_create(&tid1,NULL,read_write_num,NULL);
    pthread_create(&tid2,NULL,read_write_num,NULL);
    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);
    printf("count final val is:%d\n ",g_count);
    return 0;
}

這裏寫圖片描述

上面程序是創建兩個線程,各自都把全局靜態變量count增加5000次。正常情況下結果應該是10000,但是程序的最終結果總是5000左右,與正確結果相差很遠,這是爲什麼呢?
這是因爲對多線程的程序,發生了訪問衝突了。對一個線程要對一個變量進行加1操作,需要進行三步:
1.將數據從內存單元讀入寄存器。
2.把寄存器中的變量做加1操作。
3.把加1後的變量重新寫回內存單元。

我們來模擬一下上面程序的運行。我們上面的程序有兩個線程tid1,tid2。假如此時變量count = 10.
當tid1進行第二步時,從內存讀到10,在寄存器加1–>10+1=11。
但是此時tid2切入,從內存讀到10,在寄存器加1–>10+1=11。
因爲tid1還沒有把11寫入到內存中,tid2讀到的值並不是我們想要的11,這就引發了問題。
解決的方法就是引入互斥鎖,獲得鎖線程可以完成“讀–修改–寫”的操作,然後釋放鎖給其他線程,沒有獲得鎖的線程只能等待而不能訪問共享數據。這樣“讀–修改–寫”散步操作組成了一個原子操作,要麼都執行,要麼都不執行。

互斥鎖操作函數

互斥鎖的數據類型爲pthread_mutex_t.
創建互斥鎖:

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t* restrict mutex,const pthread_mutexattr_t* restrict attr)

返回值:成功返回0,失敗返回錯誤號。
參數:
pthread_mutex_init()函數是以動態方式創建互斥鎖的,參數attr指定了新建互斥鎖的屬性。如果參數attr爲空,則使用默認的互斥鎖屬性,默認屬性爲快速互斥鎖 。
如果Mutex變量是靜態分配的,也可以利用宏定義來初始化。

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

此方法相當於pthread_mutex_init初始化並且attr參數爲NULL.
銷燬互斥鎖:

#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);

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

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);

解鎖:

#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex);

返回值:成功返回0,失敗返回錯誤號。
一個線程可以調用pthread_mutex_lock還會獲得Mutex,若果這時另一個線程已經調用pthread_mutex_lock獲得了該Mutex,則當前進程需要掛起等待,直到另一個線程調用pthread_mutex_unlock釋Mutex,當前進程被喚醒,才能獲得該Mutex並繼續執行。
如果一個線程既想獲得鎖,又不想掛起等待,可以調用pthread_mutex_trylock,如果Mutex已經被另一個線程獲得,這個函數會失敗返回EBUSY,而不會使線程掛起等待。

#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);

這時我們就可以解決上面那個代碼的bug了。

#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>

static int g_count = 0;
pthread_mutex_t mutex_lock = PTHREAD_MUTEX_INITIALIZER;
void* read_write_num(void* _val)
{
    int val = 0;
    int i = 0;
    for(; i<5000; ++i)
    {
        pthread_mutex_lock(&mutex_lock);
        val  = g_count;
        printf("pthread id is:%x,count is : %d\n",(unsigned long)pthread_self(),g_count);
        g_count = val + 1;
        pthread_mutex_unlock(&mutex_lock);
    }
    return NULL;
}

int main()
{
    pthread_t tid1;
    pthread_t tid2;
    pthread_create(&tid1,NULL,read_write_num,NULL);
    pthread_create(&tid2,NULL,read_write_num,NULL);
    pthread_join(tid1,NULL);
    pthread_join(tid2,NULL);
    printf("count final val is:%d\n ",g_count);
    return 0;
}

這裏寫圖片描述

線程死鎖

⼀般情況下,如果同⼀個線程先後兩次調⽤lock,在第⼆次調⽤時,由於鎖已經被佔⽤,該線程會掛起等待別的線程釋放鎖,然⽽鎖正是被⾃⼰佔⽤着的,該線程又被掛起⽽沒有機會釋放鎖,因此 就永遠處於掛起等待狀態了,這叫做死鎖(Deadlock)。
另一種情況:線程A獲 得了鎖1,線程B獲得了鎖2,這時線程A調⽤lock試圖獲得鎖2,結果是需要掛起等待線程B釋放 鎖2,⽽這時線程B也調⽤lock試圖獲得鎖1,結果是需要掛起等待線程A釋放鎖1,於是線程A和B都 永遠處於掛起狀態了。
死鎖產生的4個必要條件:
1)互斥條件:指進程對所分配到的資源進行排它性使用,即在一段時間內某資源只由一個進程佔用。如果此時還有其它進程請求資源,則請求者只能等待,直至佔有資源的進程用畢釋放。
2)請求和保持條件:指進程已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它進程佔有,此時請求進程阻塞,但又對自己已獲得的其它資源保持不放。
3)不剝奪條件:指進程已獲得的資源,在未使用完之前,不能被剝奪,只能在使用完時由自己釋放。
4)環路等待條件:指在發生死鎖時,必然存在一個進程——資源的環形鏈,即進程集合{P0,P1,P2,···,Pn}中的P0正在等待一個P1佔用的資源;P1正在等待P2佔用的資源,……,Pn正在等待已被P0佔用的資源。
預防死鎖:
只要打破四個必要條件之一就能有效預防死鎖的發生:打破互斥條件:改造獨佔性資源爲虛擬資源,大部分資源已無法改造。
打破不可搶佔條件:當一進程佔有一獨佔性資源後又申請一獨佔性資源而無法滿足,則退出原佔有的資源。
打破佔有且申請條件:採用資源預先分配策略,即進程運行前申請全部資源,滿足則運行,不然就等待,這樣就不會佔有且申請。
打破循環等待條件:實現資源有序分配策略,對所有設備實現分類編號,所有進程只能採用按序號遞增的形式申請資源。

有序資源分配法


這種算 法資源按某種規則系統中的所有資源統一編號(例如打印機爲1、磁帶機爲2、磁盤爲3、等等),申請時必須以上升的次序。系統要求申請進程:
1、對它所必須使用的而且屬於同一類的所有資源,必須一次申請完;
2、在申請不同類資源時,必須按各類設備的編號依次申請。例如:進程PA,使用資源的順序是R1,R2; 進程PB,使用資源的順序是R2,R1;若採用動態分配有可能形成環路條件,造成死鎖。
採用有序資源分配法:R1的編號爲1,R2的編號爲2;
PA:申請次序應是:R1,R2
PB:申請次序應是:R1,R2
這樣就破壞了環路條件,避免了死鎖的發生

銀行家算法


避免死鎖算 法中最有代表性的算 法是Dijkstra E.W 於1968年提出的銀行家 算 法:
銀行家算法是避免死鎖的一種重要方法,防止死鎖的機構只能確保上述四個條件之一不出現,則系統就不會發生死鎖。通過這個算法可以用來解決生活中的實際問題,如銀行貸款等。
程序實現思路銀行家算法顧名思義是來源於銀行的借貸業務,一定數量的本金要應多個客戶的借貸週轉,爲了防止銀行家資金無法週轉而倒閉,對每一筆貸款,必須考察其是否能限期歸還。在操作 系統中研究資源分配策略時也有類似問題,系統中有限的資源要供多個進程使用,必須保證得到的資源的進程能在有限的時間內歸還資源,以供其他進程使用資源。如果資源分配不得到就會發生進程循環等待資源,則進程都無法繼續執行下去的死鎖現象。
把一個進程需要和已佔有資源的情況記錄在進程控制中,假定進程控制塊PCB其中“狀態”有就緒態、等待態和完成態。當進程在處於等待態時,表示系統不能滿足該進程當前的資源申請。“資源需求總量”表示進程在整個執行過程中總共要申請的資源量。顯然,每個進程的資源需求總量不能超過系統擁有的資源總數, 銀行算法進行資源分配可以避免死鎖。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章