程序員的踩坑經驗總結(四):死鎖

死鎖也是程序員最多見的問題之一了,可是死鎖跟內存泄露不一樣,原理和緣由都相對簡單,簡單說就是你等我,我也等你,就這麼耗着!html

但死鎖的影響有時比內存泄露更嚴重。內存泄露主要是漸進式的,可能重啓一下就能夠從頭開始了。而死鎖是重啓不了,這只是直接影響而已。死鎖通常會出現某個功能或者操做無反應,可能進一步沒有了心跳而下線,服務中止。而通常的看門狗也發現不了,進程還在。通常都須要手動殺進程。因此對於絕大多數的業務都是不能夠接受的。linux

而形成死鎖的緣由差異也比較大,有的可能只是程序員的一時疏忽,可有的也會讓你頭痛。程序員

咱們之前平臺的死鎖也是屢見不鮮,我記得的常見的有兩種狀況。session

(一)鎖跨度很大,代碼的跨度,看上去兩個不怎麼相關的類,居然在互相調用!還帶着鎖。我印象中咱們的流媒體出現過的一次死鎖,就是有兩個TCP session各自的兩個函數在嵌套調用。數據結構

(二)一把鎖,涉及範圍很大,鎖定一個對象的操做可能已經有四五種,可是涉及使用到的函數倒是翻倍甚至幾十個都有可能。雖然也在一個類裏面,可是類很長,帶有同一把鎖的函數之間就可能出現互相調用。分佈式

一看就知道都是設計的問題,不出問題纔怪。但是問題要解決啊,針對這些問題,後面我琢磨出了一套方法。函數

 

案例分析

案例有點久遠,當時沒有留下文檔,所幸代碼還在,針對上面第二種狀況的。因此只能是稍微描述下當時的狀況和截圖看看最後是如何解決的。優化

首先咱們看下這個類有多長:spa

    

有沒有傻眼。這又要勾起我多少痛苦的回憶。也好吧,讓大家開心一下。不過大家也開心不了多久,我都有解決之道:)設計

看看我留下的痕跡:

     

改動了31行,這還只是關於關鍵字的搜索。有多少個函數,你猜,哈。咱們主要看後面的註釋,有兩次提到「可能同時」調用或者進來。你也能夠看到,個人解決方法是使用了位運算

這一招又是從上一家學來的。其實如今看不少開源庫和內核都是大量使用了位運算,不少文檔也提到了,像Redis文件系統虛擬內存等。

咱們再來看看定義:

    

老的鎖已經放註釋裏面的了,鎖的對象是一個鏈表list。新添加了一個整型變量,把變量的幾個值定義成一個枚舉類型。

因此這幾個狀況就表明了幾種功能,這裏是四種狀況,但是實現類裏面卻有31處!你說能不死鎖嗎?

 

咱們再次還原下當時的情景。

這個list是文件列表,而它的業務無非是增刪改查。若是設計簡單的話,一把鎖也夠了。可是真正簡單設計有這麼容易嗎?

咱們又回到這個類,第一個截圖顯示2500行,根據設計基本原則,通常一個類不能超過1000行。這裏早就能夠劃分至少三個類了。

怎麼劃分,有人會建議把這個list單獨拿出去,是,我也想過。可是關係複雜了,因此咱們又到了第二張圖,你看涉及到的函數只會有增刪改查嗎?

和其餘的對象和方法交織在一塊兒了!要想抽絲剝離,只能重構!事實上,後面都重構了。

可是問題要解決。重構是後面的事,一旦出現這種嚴重問題,當下就是解決問題。因此我後面去掉了鎖,重現定義了新的變量。具體怎麼弄? 

見最後這張圖,一個變量四個值,可是這四個值可不是連續的,看到了嗎,0、一、二、4,爲何?

由於要實現二進制運算,因此他們的的二進制位對應就是,0000、000一、00十、0100。每一個值用一位表示一種操做,互不干擾。該位爲1表示佔用,若是是0表示未佔用。表明了之前的鎖狀態。

因此雖然鎖沒有了,可是(鎖的)功能仍是有的。這是一個方面,不能影響原有的功能,原來的樣子(雖然很差看,可是不能再引起其餘問題了)。另外一方面,問題也要解決,仍然是利用了這幾個位!

上面的四個值,對應的不徹底是增刪改查,具體對應了:初始化、查、刪、刪而且加四個狀態,但實際上操做是後三種。事實上初始化值0也能夠說沒有佔位。

開始咱們提到了每一個位互不干擾,如今肯定是三個位互不干擾。因此在進入某種操做時,首先判斷當前狀態,是可重入仍是須要等待

例如說,若是當前只是查,那麼繼續查(另外一個查操做)確定沒問題,而其餘兩種須要稍微等一下,這裏的等待是20次sleep的20ms循環,只要查操做結束,立刻進入下一步。

可是若是循環已經完成,而狀態依然沒變化,那麼這裏不等待了,直接退出。下次再進來詢問。

因此這裏不一樣的操做對應了不一樣的方式,因狀況而異。這樣就不會致使死鎖。同時,這些改變都須要加日誌跟蹤,能夠發現等待了多久,哪一個函數佔用時間太長,若是能減小該函數佔用的時間就是最好的了。在實際項目中,能優化的也有。但有的就只能驚訝了,有碰到過一個方法裏面有調用兩個while嵌套循環,簡單的計算也行了,有些循環裏面還調用多個方法。因此只能用這種方法了。

 

固然這個解決方法是有點抽象,因此爲了說清楚這個方法,我想了好久,其餘部分早寫完了,剩下這裏反覆改,但願你能看明白。

其實,我後面再看分佈式的鎖的實現,原理和複雜程度也不過如此:),由於咱們這些代碼早就把我給臣服了:(

 

總結和建議

(一)原理與依據

咱們上面提到了解決方法,那麼它的理論依據是什麼?

咱們稍微窺視一下鎖的實現。linux 2.6 kernel的源碼,互斥鎖所使用的數據結構:

這裏只是列出了內核中,鎖的定義,其實它的實現還有不少。有興趣的能夠看源碼。咱們回到這個主題,不知你們發現沒有,其實鎖的本質也是一個整型變量

而我就是利用了這個特性,固然也有一點自旋鎖的特性。你能夠再往會看,第二張圖,其中有三處for循環,就是說我會根據狀況進行判斷和等待一會,但不是忙等待,就是說到了必定的時間後,我會強制改變狀態和退出。因此和自旋鎖又有不一樣

因此總結一下,原理很重要!

(二)死鎖的預防

和內存泄露同樣,死鎖的預防也在於設計。因此代碼的質量在於設計!這裏一樣只針對死鎖的問題提幾個建議。

1.減小鎖定代碼的範圍

鎖定的代碼行數,必定用到的時候才用,只將相關的變量括起來。而不是鎖定整個函數。

寫段僞代碼說明下。

std::mutex  m_mutex;

int  g_diff = 3;

int funA()

{

unique_lock<mutex> lock(m_mutex);

int a = 5;

//中間省去若干

return a+g_diff; 

} 

int funB()

{

int a = 5;

int b = 0;

  {

    unique_lock<mutex> lock(m_mutex);

    b = a+g_diff; 

  }

//中間省去若干

return b;

} 

函數funB確定比函數funA更好。

2.下降鎖的粒度

一般,一個變量一把鎖,或者一個功能點一把鎖,而不是一個類一把鎖。

那有的人會說若是要鎖住一個類,怎麼辦?

我見過的只有在一種狀況下一個類才須要用到鎖,就是把這個類當變量使用。因此這種狀況也能夠概括到一個變量,或者說一個對象。而這種狀況通常用在單例模式中,因此即便鎖住也不可能出現方法的嵌套而致使死鎖。關於單例模式的使用,我後面還有文章將會介紹。很快,後面第二篇吧。

並且這裏說的一個變量,或者一個功能點要職責單一。一個類未嘗不是如此!

案例裏面其實就是函數的功能模糊,類的職責模糊,估計當時都沒有設計,反正把相關的都放一塊兒,一鍋亂燉!

因此這是設計和開發裏面的大忌!後面就是改不完的Bug、踩不完的坑。。

3.減小鎖的使用

儘可能不用鎖、少用鎖。非用不可才用鎖。

一方面由於多了容易形成死鎖,另外一方面鎖有必定的消耗。上面提到的源碼只是一個定義而已,而它的實現不只僅有幾處循環,還有回調函數。

固然,這一點提及來容易,作起來難!具體怎麼少用,有沒有好的方法?

個人回答固然是有,請聽下回分解。

相關文章
相關標籤/搜索