辨析:自旋鎖與信號量

自旋鎖(Spinlock)
信號量(Semaphore):

1. 自旋鎖與信號量簡介

自旋是鎖的一種實現方式,通過忙等待(“自旋,spinning”)來實現【例如通過while循環持續請求獲取鎖】。

信號量的概念比鎖的範圍更大, 可以說, 鎖是信號量的一種特殊情況。

一般來說,自旋鎖只在進程內有效,而信號量可同於控制多個進程之間的同步。

鎖主要用於互斥操作,也就是說,每次只允許一個線程持有這個鎖(的鑰匙)並繼續執行代碼的“關鍵部分(critical section)”。
關鍵部分是指可以由多個線程執行的, 修改共享數據的那部分代碼。

信號量有一個計數器(counter),同一時刻最多允許被N個線程獲取,具體是多少線程則取決於設置的值, 在某些實現中這個值就是允許的最大值。

從這點來看,可以認爲信號量的一種特殊情況,即信號量計數器最大值爲1的情形。

2. 常規情況下的行爲

如上所述,自旋鎖是一種鎖,也就是一種互斥機制。
實現原理是重複嘗試 “查詢並修改內存位置”, 一般來說這種 “查詢並修改” 是原子操作。
通過自旋來獲取鎖, 是一致“忙碌”的操作,可能長時間(甚至永遠!)佔用CPU,實際上卻什麼有效工作都沒做。

使用自旋的原因在於:

  • 上下文切換的開銷,相當於上千次循環操作的開銷,如果可以通過浪費少量的CPU週期來獲得鎖,則總體來說更划算。
  • 另外,實時計算程序明顯是不能接受阻塞和喚醒的, 而且這種喚醒還是在不確定的時間才輪到調度程序來通知。

相比之下,信號量可以不需要進行旋轉,或者僅旋轉很短的時間(作爲一種避免系統調用的優化)。
如果無法獲取信號量,則線程會阻塞,從而將CPU時間讓給其他準備運行的線程。
當然,這可能意味着在重新安排線程前要經過幾毫秒,如果這不是問題, 那麼它的效率可能更高,這是一種節省CPU資源的實現方法。

3. 在競爭激烈時的行爲

常見的誤解是自旋鎖或無鎖算法的“速度會更快”,或者誤以爲他們僅對“非常短的任務”有用。
比如, 理想情況下,沒有哪個同步對象鎖的保留時間超過必要的時間。

在競爭激烈的情況下, 自旋鎖和信號量實現的行爲差異很大。

設計良好的系統一般很少阻塞,或者沒有阻塞。也就是說不應該發生所有線程都在同一時間爭搶鎖資源的情況。
比如我們一般都不會編寫這樣的代碼: 先獲取鎖之後,再從網上下載很大的zip壓縮文件,接着轉碼解析,再去修改共享資源(比如將數據加到列表/Map中),最後才釋放鎖。
應該採用的策略是:只在訪問共享資源時,纔去獲取鎖。

在關鍵部分外面的工作量,要比關鍵部分內部的工作量要大,才比較合理。
自然,一個線程處於關鍵部分的執行時間要非常短, 這樣的話也就很少發生多個線程同時爭用鎖的情況。
當然,偶爾也會有多個線程同時嘗試獲取鎖(假如沒有這種情況的話,那就不需要鎖了!);在“健康”的系統中這是很少發生的情景。

在沒有激烈鎖爭用的情況下,自旋鎖的性能要大大優於信號量; 因爲沒有鎖擁塞,獲取自旋鎖的開銷僅爲幾十個CPU週期, 而上下文切換的開銷則至少幾百/上千個時鐘週期,而且操作系統的時間片切換週期還有可能會丟棄幾千萬個時鐘週期。

如果擁塞程度很高,或者鎖會被長時間持有(有時候真的沒辦法!),在這種場景下,使用自旋鎖則會消耗大量的CPU時間,卻什麼活都沒幹。

這時候使用信號量(或互斥鎖)是一種更好的解決辦法,因爲沒有搶佔CPU,其他線程在這段時間內就可以有效運行。
如果沒有需要使用CPU的線程,則操作系統會降低CPU的速度,以減少熱量並節約電費。

在單核CPU的系統上,自旋鎖在鎖擁塞的情況下效率會更低,因爲自旋的線程會將所有時間都浪費在等待狀態改變。 除非另一個線程被調度,執行完關鍵部分並釋放鎖之後,這個線程的情況纔可能會改變。
因此,在有爭用的場景中,通過信號量獲取鎖, 最好的情況大約需要1~2個時間片(假設釋放鎖的線程恰好在下一個時間片被調度),但這種情況並不理想。

4. 自旋鎖與信號量的實現方式

在Linux系統中, 信號量實現通常會用到 sys_futex(可選帶有自旋鎖,在嘗試一定次數後退出)。

自旋鎖通常使用原子操作來實現, 而不使用操作系統提供的系統函數。
以前的實現需要使用編譯器內部函數或者不可移植的彙編指令。
而在 C++11 和 C11 中,都將原子操作作爲語言的一部分。
因此,現在實現可移植的無鎖代碼已經很方便了。 當然,編寫可證明其正確性的代碼還是有一點難度的。

線程切換的代價

Linux 時間片默認0.75~6ms; Win XP大約10-15ms左右; 可能高可能低,但量級在ms級。 假設CPU是2GHZ,則每時間片大約對應2M個時鐘週期。

JDK的信號量實現是經過優化的,實際上先進行了一定量的自旋操作。 好處是充分利用了操作系統分配給當前線程的時間片,否則這個時間片就被浪費了。

如果進行多個線程的 synchronized 和 wait-notify 切換測試,會發現程序的性能基本上不受時間片週期的影響。

Stackoverflow上的問題鏈接: https://stackoverflow.com/questions/195853/spinlock-versus-semaphore

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