一個死鎖的問題

問題是這樣的,如下圖所示:

1.有一個線程模型,其中,MainThread爲主線程,他有一些資源,比如兩個互斥器mutex1,mutex2,下面統稱爲鎖。

2.這個主線程可以創建很多子線程thread1、thread2......threadn,主線程還有很多回調函數callback1、callback2......callbackn,這些回調函數在創建子線程時被掛到子線程上。

3.這些子線程會互斥的輪詢一段代碼C,C中會訪問主線程中的一些資源。可想而知,在輪詢時,他們需要加鎖,於是每個線程就爭搶mutex1這個鎖。

4.代碼C中做了一件事情,它會嘗試調掛在它上面的回調函數(比如thread1執行代碼段C時,就去調callback1),由於回調函數都在主線程,而觸發回調是在各個子線程,因此這裏就需要線程同步,於是,會有一個線程同步隊列。各個子線程將觸發回調的消息投遞到這個用於線程同步的隊列中(入隊),可想而知,他們也是互斥的,因此在投遞時,各個子線程需要爭奪mutex2這個鎖,因爲是異步投遞,所以投遞後立刻釋放mutex2,並返回到它自己的線程中繼續執行代碼C中後面的代碼,代碼C結束後,釋放mutex1這個鎖。

5.用於線程同步的隊列會不斷的取出隊首的回調並觸發它(出隊),且入隊和出隊是多線程的,所以出隊時也要爭搶到mutex2這個鎖纔可以做。得到mutex2後,出隊首元素,觸發其回調,回調執行完後,釋放mutex2。



上述5點就描述了這個線程模型的基本運作情況,總結起來就是有兩套獨立的輪詢:

第一套:

各個子線程先搶mutex1

-->持有mutex1後

-->再搶mutex2

->持有mutex2後

-->投遞到線程同步隊列

-->釋放mutex2

-->釋放mutex1

第二套:

線程同步隊列搶mutex2鎖

-->持有mutex2後

-->出隊,觸發回調

-->回調結束後釋放mutex2


這種模型可以良好的工作不發生死鎖。因爲根據死鎖產生的原因,必然是至少有兩個操作在同時爭搶至少兩個鎖,且各自佔有一個鎖時,會發生死鎖,如下

上面的模型中沒有這種情況,所以不會死鎖。


現在加一個操作M,可以認爲它是一個函數,在主線程中執行,並會訪問主線中的一些資源,所以,這段代碼也需要加mutex1鎖執行。當M單獨被觸發時,它與多個子線程爭搶mutex1,並總有機會得到,所以不會死鎖。


現在考慮將操作M放到各個回調函數中執行,則第二套輪詢變爲:

線程同步隊列搶mutex2鎖

-->持有mutex2後

-->出隊,觸發回調

-->回調中執行M,搶mutex1鎖

-->持有mutex1後

-->執行代碼段D

-->釋放mutex1後

-->回調結束後釋放mutex2


則第一套輪詢和第二套輪詢會同時相互爭搶mutex1和mutex2,那麼總有一個時刻,第一套輪詢持有mutex1,等mutex2,第二套輪詢持有mutex2,等mutex1,則發生死鎖。


一開始查原因時,懷疑是代碼段C重入的問題,於是將boost::mutex改爲boost::recursive_mutex來允許重入,但是依然沒有解決問題。後來發現線程同步時採用的是異步投遞,及投遞完就返回,代碼C並不會因爲投遞後觸發回調而被阻塞,那麼就不會有重入的問題。

最後發現問題出現在從線程同步隊列中出隊並觸發回調這裏

僞代碼如下:

mutex2.lock();//獲得mutex2鎖

pCallBack = queue.front()//出隊

pCallBack();//觸發回調,阻塞在這裏,等待回調執行完

mutex2.unlock()//回調執行完後釋放mutex2


那麼當執行回調時,拿住了mutex2(即回調將mutex2被鎖住的狀態帶了出去),回調中的M操作又去請求mutex1鎖而這時,由於多個子線程還在不斷輪詢,則可以預料某一個子線程拿住了mutex1鎖,在請求mutex2鎖,於是發生死鎖。


其實解決方案也很簡單,由上面的第一張圖中標紅處和上面的僞代碼可知,mutex2解鎖如果提前的話,就不會有問題。及在出隊後,先釋放mutex2,再觸發回調,這樣回調就不會把mutex2被鎖住的狀態帶出去)。僞代碼如下:

mutex2.lock();//獲得mutex2鎖

pCallBack = queue.front()//出隊

mutex2.unlock()//釋放mutex2

pCallBack();//觸發回調,阻塞在這裏,等待回調執行完


這個例子其實說明了一個很簡單的道理:嚴格控制粒度,僅對必須加鎖的代碼加鎖


從發現這個問題到解決花了將近兩天時間,一開始以爲是重入的問題,於是研究了boost::recursive_mutex和boost::mutex的區別,並替換爲boost::recursive_mutex,但還是不行,中間花了很多時間學習boost的鎖,並對自己的多線程基礎知識產生了質疑。不過也暴露了在多線程調試經驗上的不足,方法不多。後來嘗試在代碼C異步投遞之後向文件打log查看異步投遞是否返回的方式,發現只要是加上了這個打log的操作,就不會死鎖,於是順着這條線,終於查到了原因。現在回過頭看打Log後不死鎖這件事,我認爲這是一個時間差的原因,及這種方式不是不會產生死鎖,原來沒打log時也不是必然產生死鎖,只是一個時間差導致的概率問題,打Log後,可能因爲線程操作所需時間改變,造成爭搶mutex1的時間剛好合適到子線程不會拿到mutex1,所以M操作順利拿到兩個鎖,於是沒死鎖。沒死鎖不代表邏輯沒問題,只是死鎖概率降低了,相信重複操作量達到一定程度時,死鎖還是會出現。

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