多線程中鎖那些事兒

我們就從一個生活例子入手。
你長大了,但是因爲沒有在大學好好學習的緣故,你入職了一家偏僻的小公司,辦公環境還說的過去,但是至於廁所就只有一個了,更慘的事情是廁所裏只有一個馬桶,你們公司可以比作同一個進程空間,每個員工就是進程中不同的線程,幹着各自的活,洗手間就是臨界區,馬桶就是共享變量(臨界資源),當有一個人進廁所後就必須上鎖,其他人如果也想上廁所首先就會看廁所的鎖有沒有上,如果上了,他就不會繼續去訪問廁所,如果廁所沒鎖,或者裏面的人忘記了上鎖,你應該想像那畫面,有內味兒。。。。。
所以這個故事告訴我們要好好學習,找個擁有大廁所的公司。
上面純屬打趣兒。
編程中鎖的重要性遠非上面的例子那麼一點點,但是和生活中加鎖的目的是一樣的,鎖是保證代碼安全性的重要因素。

下面進入正題。

1, 線程間通信

線程間通信的方式:

方式 區別
1、臨界區 通過多線程的串行化來訪問公共資源或一段代碼,速度快,適合控制數據訪問;
2、互斥量 (互斥鎖)Synchronized/Lock 採用互斥對象機制,只有擁有互斥對象的線程纔有訪問公共資源的權限。因爲互斥對象只有一個,所以可以保證公共資源不會被多個線程同時訪問
3、信號量 Semphare 爲控制具有有限數量的用戶資源而設計的,它允許多個線程在同一時刻去訪問同一個資源,但一般需要限制同一時刻訪問此資源的最大線程數目。
4、事件(信號),Wait/Notify 通過通知操作的方式來保持多線程同步,還可以方便的實現多線程優先級的比較操作

2, Linux的4種鎖機制

種類 介紹
互斥鎖:mutex 用於保證在任何時刻,都只能有一個線程訪問該對象。當獲取鎖操作失敗時,線程會進入睡眠,等待鎖釋放時被喚醒
讀寫鎖:rwlock 分爲讀鎖和寫鎖。處於讀操作時,可以允許多個線程同時獲得讀操作。但是同一時刻只能有一個線程可以獲得寫鎖。其它獲取寫鎖失敗的線程都會進入睡眠狀態,直到寫鎖釋放時被喚醒。 注意:寫鎖會阻塞其它讀寫鎖。當有一個線程獲得寫鎖在寫時,讀鎖也不能被其它線程獲取;寫者優先於讀者(一旦有寫者,則後續讀者必須等待,喚醒時優先考慮寫者)。適用於讀取數據的頻率遠遠大於寫數據的頻率的場合。
自旋鎖:spinlock 在任何時刻同樣只能有一個線程訪問對象。但是當獲取鎖操作失敗時,不會進入睡眠,而是會在原地自旋,直到鎖被釋放。這樣節省了線程從睡眠狀態到被喚醒期間的消耗,在加鎖時間短暫的環境下會極大的提高效率。但如果加鎖時間過長,則會非常浪費CPU資源。
RCU:即read-copy-update 在修改數據時,首先需要讀取數據,然後生成一個副本,對副本進行修改。修改完成後,再將老數據update成新的數據。使用RCU時,讀者幾乎不需要同步開銷,既不需要獲得鎖,也不使用原子指令,不會導致鎖競爭,因此就不用考慮死鎖問題了。而對於寫者的同步開銷較大,它需要複製被修改的數據,還必須使用鎖機制同步並行其它寫者的修改操作。在有大量讀操作,少量寫操作的情況下效率非常高。

互斥鎖和讀寫鎖的區別

  • 互斥鎖:mutex,用於保證在任何時刻,都只能有一個線程訪問該對象。當獲取鎖操作失敗時,線程會進入睡眠,等待鎖釋放時被喚醒。
    另外,繼續上面的例子,如果有人想去洗手間時發現門鎖上了,他也有兩種策略:1,在洗手間那裏等(阻塞); 2,暫時先離開等會再過來看(非阻塞);這就是阻塞鎖和非阻塞鎖。
  • 讀寫鎖:rwlock,分爲讀鎖和寫鎖。處於讀操作時,可以允許多個線程同時獲得讀操作。但是同一時刻只能有一個線程可以獲得寫鎖。其它獲取寫鎖失敗的線程都會進入睡眠狀態,直到寫鎖釋放時被喚醒。 注意:寫鎖會阻塞其它讀寫鎖。當有一個線程獲得寫鎖在寫時,讀鎖也不能被其它線程獲取;寫者優先於讀者(一旦有寫者,則後續讀者必須等待,喚醒時優先考慮寫者)。適用於讀取數據的頻率遠遠大於寫數據的頻率的場合。

3, 死鎖

假設有A、B、C3個人在一起喫飯,每個人左右各有一隻筷子。所以,這其中要是有一個人想喫魚,他必須首先拿起左邊的筷子,再拿起右邊的筷子。現在,我們讓所有的人同時開始喫飯。那麼就很有可能出現這種情況。每個人都拿起了左邊的筷子,或者每個人都拿起了右邊的筷子,爲了喫飯,他們現在都在等另外一隻筷子。此時每個人都想喫飯,同時每個人都不想放棄自己已經得到的一那隻筷子。所以,事實上大家都吃不了飯。就造成了死鎖。這裏筷子就是共享資源,三個人就是三個線程。

在這裏插入圖片描述

3.1 死鎖產生的4個必要條件

死鎖是指兩個或兩個以上進程在執行過程中,因爭奪資源而造成的下相互等待的現象。死鎖發生的四個必要條件如下:

四個必要條件 介紹
互斥條件 進程對所分配到的資源不允許其他進程訪問,若其他進程訪問該資源,只能等待,直至佔有該資源的進程使用完成後釋放該資源;
請求和保持條件 (佔有且等待) 進程獲得一定的資源後,但是還有資源未得到滿足,正在請求其他進程釋放該資源。此時請求阻塞,但該進程不會釋放自己已經佔有的資源
不可剝奪條件 (不可搶佔) 進程已獲得的資源,在未完成使用之前,不可被剝奪,只能在使用後自己釋放 換個方式說,就是別人已經佔有了某項資源,你不能因爲自己也需要該資源,就去把別人的資源搶過來。
環路等待條件(循環等待) 進程發生死鎖後,必然存在一個進程-資源之間的環形鏈, 使得每個進程都佔有下一個進程所需的至少一種資源。
  • 當以上四個條件均滿足,必然會造成死鎖,發生死鎖的進程無法進行下去,它們所持有的資源也無法釋放。這樣會導致CPU 的吞吐量下降。所以死鎖情況是會浪費系統資源和影響計算機的使用性能的。那麼,解決死鎖問題就是相當有必要的了。

3.2 死鎖解決

產生死鎖需要四個條件,那麼,只要這四個條件中至少有一個條件得不到滿足,就不可能發生死鎖了。由於互斥條件是非共享資源所必須的,不僅不能改變,還應加以保證,所以,主要是破壞產生死鎖的其他三個條件。

  • 破壞“佔有且等待”條件

方法1:所有的進程在開始運行之前,必須一次性地申請其在整個運行過程中所需要的全部資源。

  • 優點:簡單易實施且安全。
  • 缺點:因爲某項資源不滿足,進程無法啓動,而其他已經滿足了的資源也不會得到利用,嚴重降低了資源的利用率,造成 資源浪費。使進程經常發生飢餓現象。

方法2:該方法是對第一種方法的改進,允許進程只獲得運行初期需要的資源,便開始運行,在運行過程中逐步釋放掉分配到的已經使用完畢的資源,然後再去請求新的資源。這樣的話,資源的利用率會得到提高,也會減少進程的飢餓問題。

  • 破壞“不可搶佔”條件

當一個已經持有了一些資源的進程在提出新的資源請求沒有得到滿足時,它必須釋放已經保持的所有資源,待以後需要使用 的時候再重新申請。這就意味着進程已佔有的資源會被短暫地釋放或者說是被搶佔了。

  • 該種方法實現起來比較複雜,且代價也比 較大。釋放已經保持的資源很有可能會導致進程之前的工作失效等,反覆的申請和釋放資源會導致進程的執行被無限的推遲,這 不僅會延長進程的週轉週期,還會影響系統的吞吐量。
  • 破壞“循環等待”條件

可以通過定義資源類型的線性順序來預防,可將每個資源編號,當一個進程佔有編號爲i的資源時,那麼它下一次申請資源只 能申請編號大於i的資源。

總結

(1)死鎖的危險始終存在,但是我們應該儘量減少這種危害存在的範圍
(2)解決死鎖花費的代價是異常高昂的
(3)最好的死鎖處理方法就是在編寫程序的時候儘可能檢測到死鎖
(4)多線程是一把雙刃劍,有了效率的提高當然就有死鎖的危險
(5)某些程序的死鎖是可以容忍的,大不了重啓機器,但是有些程序不行
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章