在開發的過程中,很常見的場景就是在多進程或者多線程中訪問同一份資源,而如果直接不加限制的對這段資源進行寫操作的話,很可能會將這段共享資源寫亂而導致不可預期的後果。在Linux中爲了解決這個問題,一個常用的方法就是對操作這段共享資源的區域進行加鎖避免上述問題。在Linux中將鎖在不同的角度進行了一些分類,這裏記錄一下Linux中提到的一些鎖的概念以及其特點。
本文只對部分類型的鎖的概念、特點進行記錄,而不深究其實現。如果有什麼理解有誤的地方歡迎指正。
1、自旋鎖(spinlock)與互斥鎖
1.1 概念
自旋鎖設計的初衷是在短期內進行輕量級的鎖定(後面介紹自旋鎖的特點就能清楚的明白爲什麼是在短期內了),它將一段臨界區代碼進行鎖定,保證當前線程/進程執行這段臨界區代碼的時候不會被其他線程/進程中斷,避免因爲競爭導致共享資源被破壞。自旋鎖被廣泛地運用在Linux底層的同步機制,你可以看到許多內核的數據結構中都嵌有spinlock,這些大部分是用於保證它自身被操作的原子性,在操作這些結構的時候通常都會經過這幾個步驟:加鎖->操作->解鎖。
互斥鎖一般通過互斥量mutex來實現,互斥鎖的實現也是爲了避免多進程/多線程訪問共享資源的時候,由於數據競爭而導致的數據被破壞而設計的。通常程序中我們通過互斥量對臨界區進行加鎖,臨界區代碼執行完成後,對其進行一個解鎖操作。
從上面看自旋鎖和互斥鎖的功能基本上類似,但是它們之間的區別是什麼呢?首先我們描述一下鎖的一個工作過程:
- 1、初始狀態下,鎖的狀態爲未被佔有
- 2、A進程需要訪問臨界區代碼,嘗試獲取鎖。此時發現鎖可用,則將鎖lock,進入臨界區執行臨界區代碼
- 3、B進程需要訪問臨界區代碼,嘗試獲取鎖。此時發現鎖呢已經被A佔有,則等待A釋放鎖
- 4、A進程退出臨界區,釋放鎖.unlock
- 5、B進程獲得鎖進入執行臨界區代碼,並lock
- 6、B進程執行晚臨界區代碼,釋放鎖unlock
對於自旋鎖和互斥鎖,主要的區別體現在第3步和第5步。
第3步,當進程B發現鎖已經被進程A佔有:對於自旋鎖來說,B進程會"原地旋轉",即執行循環,去檢測鎖是否已經被釋放;對於互斥鎖來說,B進程直接進入sleep休眠狀態,將CPU的使用權交由其他進程處理,等待鎖被釋放是被喚醒。
第5步,B進程被觸發獲得鎖的方式:對於自旋鎖來說,B進程自己檢測待鎖已經處於無人佔有的狀態,敏感度較高;而對於互斥鎖來說,B進程是被系統重新喚醒,敏感度較差。
1.2 特點
- 1、自旋鎖等待過程中耗費CPU,而互斥鎖不會(原因:自旋鎖等待鎖的過程中循環檢測鎖的狀態,)
- 2、自旋鎖常用在臨界區較爲短小的場景下 (原因:等待自旋鎖的時間過長,會浪費過多CPU)
- 3、自旋鎖中等待的進程/線程獲取鎖的靈敏度較高(原因:自身循環檢測)
- 4、自旋鎖在非搶佔式的單核處理器中不起作用。(原因:等待線程在循環等待,但是又不允許搶佔,會導致CPU卡主,所以這種架構的處理器中自旋鎖被實現爲空)
- 5、自旋鎖和互斥鎖都只允許一次只有一個進程/線程進入臨界區。
- 6、自旋鎖在"喚醒"時不需要進行上下文切換,而互斥鎖需要進行上下文切換,切換成本較高。
2、樂觀鎖與悲觀鎖
2.1 概念
與自旋鎖&互斥鎖不同的是,樂觀鎖和悲觀鎖不是哪種類型的鎖的實現,而是操作共享數據的一種思想。
悲觀鎖以一種悲觀的思想,認爲對數據進行操作(讀取/更新)的時候,很大可能會有其他進程/線程會對該數據進行修改。所以在悲觀鎖思想中,對數據進行操作之前先對數據進行加鎖。只有獲取鎖成功,才允許對該數據進行操作;若獲取鎖失敗則被阻塞或者報錯。
樂觀鎖則以一種樂觀的思想,認爲對數據進行操作的時候,數據不會被其他進程或者線程修改。樂觀鎖對於讀取數據的場景,讀取前不需要加鎖,直接讀取數據;對於更新數據的場景,也不進行加鎖,而是通過版本號控制或者CAS的方式對數據進行更新。
備註:cas(compare and set)機制更新數據的機制的過程簡單描述如下:首先客戶端讀取出當前數據的當前版本,然後本地對數據進行修改,修改完畢後,將數據和版本號同時發送到服務端進行更新,服務端發現版本和當前server端數據版本不一致,則說明server端數據被修改過,則直接報錯不允許更新;若版本一致則更新成功。
2.2 特點
- 1、讀多寫少的場景下,建議使用樂觀鎖,此時樂觀鎖效率要高於悲觀鎖,可以提高系統的吞吐量
- 2、寫多讀少(寫數據衝突較高)的場景下,建議使用悲觀鎖,因爲寫衝突高會導致cas產生大量碰撞從而導致大量失敗重試
- 3、樂觀鎖更新數據存在ABA的問題:客戶端取到數據爲A,在更新的時候檢查數據內容也是A,在客戶端角度數據沒有發生變化,但是有可能數據中間被修改成了B然後又變成了A。
- 4、樂觀鎖中只能保證一個共享變量的原子操作 CAS 只對單個共享變量有效,當操作涉及跨多個共享變量時 CAS 無效。
- 5、樂觀鎖如果使用自旋cas(概念與上一節類似),如果更新長時間不成功,CPU資源會浪費比較大。
3、讀寫鎖
3.1 概念
在很多場景中,對共享數據的操作進行讀操作較多,寫操作很少;由於在讀數據的時候不會產生髒數據,所以多線程同時對數據進行讀操作是沒有問題的。所以在這種場景下如果一味的使用互斥鎖,會導致對數據的操作效率很低。因此這種情況下產生了讀寫鎖,讀寫鎖與互斥鎖類似,也是對進程/線程進入一段臨界區的訪問控制。
3.2 特點
讀寫鎖有三種狀態:添加讀鎖,添加寫鎖,未加鎖
- 1、只要沒有添加寫鎖,所有讀線程都允許進入臨界區進行操作。(允許多讀,寫的時候不允許讀)
- 2、只有在未加鎖狀態下,才允許寫線程進入臨界區。(只允許一個進程寫,寫的過程中不允許有其他任何操作)
- 3、Linux中pthread_rwlock_t讀寫鎖本質上是一個自旋鎖。
- 4、儘量在讀多寫少的場景下使用讀寫鎖
- 5、在讀和寫操作進行競爭鎖的時候,寫操作優先獲得鎖。
參考文章:
https://zhuanlan.zhihu.com/p/110123628
http://www.jeepxie.net/article/225836.html
http://www.wowotech.net/kernel_synchronization/445.html
https://www.cnblogs.com/cposture/p/SpinLock.html
http://www.wowotech.net/kernel_synchronization/spinlock.html
https://cslqm.github.io/2020/01/06/spin-lock/
https://learnku.com/articles/27880
https://blog.csdn.net/dangzhangjing97/article/details/80368822