死鎖也是程序員最多見的問題之一了,可是死鎖跟內存泄露不一樣,原理和緣由都相對簡單,簡單說就是你等我,我也等你,就這麼耗着!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循環,就是說我會根據狀況進行判斷和等待一會,但不是忙等待,就是說到了必定的時間後,我會強制改變狀態和退出。因此和自旋鎖又有不一樣。
因此總結一下,原理很重要!
和內存泄露同樣,死鎖的預防也在於設計。因此代碼的質量在於設計!這裏一樣只針對死鎖的問題提幾個建議。
鎖定的代碼行數,必定用到的時候才用,只將相關的變量括起來。而不是鎖定整個函數。
寫段僞代碼說明下。
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更好。
一般,一個變量一把鎖,或者一個功能點一把鎖,而不是一個類一把鎖。
那有的人會說若是要鎖住一個類,怎麼辦?
我見過的只有在一種狀況下一個類才須要用到鎖,就是把這個類當變量使用。因此這種狀況也能夠概括到一個變量,或者說一個對象。而這種狀況通常用在單例模式中,因此即便鎖住也不可能出現方法的嵌套而致使死鎖。關於單例模式的使用,我後面還有文章將會介紹。很快,後面第二篇吧。
並且這裏說的一個變量,或者一個功能點要職責單一。一個類未嘗不是如此!
案例裏面其實就是函數的功能模糊,類的職責模糊,估計當時都沒有設計,反正把相關的都放一塊兒,一鍋亂燉!
因此這是設計和開發裏面的大忌!後面就是改不完的Bug、踩不完的坑。。
儘可能不用鎖、少用鎖。非用不可才用鎖。
一方面由於多了容易形成死鎖,另外一方面鎖有必定的消耗。上面提到的源碼只是一個定義而已,而它的實現不只僅有幾處循環,還有回調函數。
固然,這一點提及來容易,作起來難!具體怎麼少用,有沒有好的方法?
個人回答固然是有,請聽下回分解。