首頁 > 軟體

Golang自旋鎖的相關介紹

2022-10-12 14:00:32

自旋鎖

獲取鎖的執行緒一直處於活躍狀態,但是並沒有執行任何有效的任務,使用這種鎖會造成busy-waiting。 它是為實現保護共用資源而提出的一種鎖機制。其實,自旋鎖與互斥鎖比較類似,它們都是為了解決某項資源的互斥使用。無論是互斥鎖,還是自旋鎖,在任何時刻,最多隻能由一個保持者,也就說,在任何時刻最多隻能有一個執行單元獲得鎖。但是兩者在排程機制上略有不同。對於互斥鎖,如果資源已經被佔用,資源申請者只能進入睡眠狀態。但是自旋鎖不會引起呼叫者睡眠,如果自旋鎖已經被別的執行單元保持,呼叫者就一直迴圈在那裡看是否該自旋鎖的保持者已經釋放了鎖,“自旋”一詞就是因此而得名。

golang實現自旋鎖

type spinLock uint32
func (sl *spinLock) Lock() {
    for !atomic.CompareAndSwapUint32((*uint32)(sl), 0, 1) {
        runtime.Gosched()
    }
}
func (sl *spinLock) Unlock() {
    atomic.StoreUint32((*uint32)(sl), 0)
}
func NewSpinLock() sync.Locker {
    var lock spinLock
    return &lock
}

可重入的自旋鎖和不可重入的自旋鎖

上面的程式碼,仔細分析一下就可以看出,它是不支援重入的,即當一個執行緒第一次已經獲取到了該鎖,在鎖釋放之前又一次重新獲取該鎖,第二次就不能成功獲取到。由於不滿足CAS,所以第二次獲取會進入while迴圈等待,而如果是可重入鎖,第二次也是應該能夠成功獲取到的。

而且,即使第二次能夠成功獲取,那麼當第一次釋放鎖的時候,第二次獲取到的鎖也會被釋放,而這是不合理的。

為了實現可重入鎖,我們需要引入一個計數器,用來記錄獲取鎖的執行緒數

type spinLock struct {
      owner int
      count  int
}
func (sl *spinLock) Lock() {
        me := GetGoroutineId()
        if spinLock .owner == me { // 如果當前執行緒已經獲取到了鎖,執行緒數增加一,然後返回
               sl.count++
               return
        }
        // 如果沒獲取到鎖,則通過CAS自旋
    for !atomic.CompareAndSwapUint32((*uint32)(sl), 0, 1) {
        runtime.Gosched()
    }
}
func (sl *spinLock) Unlock() {
      if  rl.owner != GetGoroutineId() {
          panic("illegalMonitorStateError")
      }
      if sl.count >0  { // 如果大於0,表示當前執行緒多次獲取了該鎖,釋放鎖通過count減一來模擬
           sl.count--
       }else { // 如果count==0,可以將鎖釋放,這樣就能保證獲取鎖的次數與釋放鎖的次數是一致的了。
           atomic.StoreUint32((*uint32)(sl), 0)
       }
}
func GetGoroutineId() int {
    defer func()  {
        if err := recover(); err != nil {
            fmt.Println("panic recover:panic info:%v", err)     }
    }()
    var buf [64]byte
    n := runtime.Stack(buf[:], false)
    idField := strings.Fields(strings.TrimPrefix(string(buf[:n]), "goroutine "))[0]
    id, err := strconv.Atoi(idField)
    if err != nil {
        panic(fmt.Sprintf("cannot get goroutine id: %v", err))
    }
    return id
}
func NewSpinLock() sync.Locker {
    var lock spinLock
    return &lock
}

自旋鎖的其他變種

1. TicketLock

TicketLock主要解決的是公平性的問題。

思路:每當有執行緒獲取鎖的時候,就給該執行緒分配一個遞增的id,我們稱之為排隊號,同時,鎖對應一個服務號,每當有執行緒釋放鎖,服務號就會遞增,此時如果服務號與某個執行緒排隊號一致,那麼該執行緒就獲得鎖,由於排隊號是遞增的,所以就保證了最先請求獲取鎖的執行緒可以最先獲取到鎖,就實現了公平性。

可以想象成銀行辦業務排隊,排隊的每一個顧客都代表一個需要請求鎖的執行緒,而銀行服務視窗表示鎖,每當有視窗服務完成就把自己的服務號加一,此時在排隊的所有顧客中,只有自己的排隊號與服務號一致的才可以得到服務。

2. CLHLock

CLH鎖是一種基於連結串列的可延伸、高效能、公平的自旋鎖,申請執行緒只在本地變數上自旋,它不斷輪詢前驅的狀態,如果發現前驅釋放了鎖就結束自旋,獲得鎖。

3. MCSLock

MCSLock則是對本地變數的節點進行迴圈。

4. CLHLock 和 MCSLock

都是基於連結串列,不同的是CLHLock是基於隱式連結串列,沒有真正的後續節點屬性,MCSLock是顯示連結串列,有一個指向後續節點的屬性。

將獲取鎖的執行緒狀態藉助節點(node)儲存,每個執行緒都有一份獨立的節點,這樣就解決了TicketLock多處理器快取同步的問題。

自旋鎖與互斥鎖

  • 自旋鎖與互斥鎖都是為了實現保護資源共用的機制。
  • 無論是自旋鎖還是互斥鎖,在任意時刻,都最多隻能有一個保持者。
  • 獲取互斥鎖的執行緒,如果鎖已經被佔用,則該執行緒將進入睡眠狀態;獲取自旋鎖的執行緒則不會睡眠,而是一直迴圈等待鎖釋放。

總結

  • 自旋鎖:執行緒獲取鎖的時候,如果鎖被其他執行緒持有,則當前執行緒將回圈等待,直到獲取到鎖。
  • 自旋鎖等待期間,執行緒的狀態不會改變,執行緒一直是使用者態並且是活動的(active)。
  • 自旋鎖如果持有鎖的時間太長,則會導致其它等待獲取鎖的執行緒耗盡CPU。
  • 自旋鎖本身無法保證公平性,同時也無法保證可重入性。
  • 基於自旋鎖,可以實現具備公平性和可重入性質的鎖。
  • TicketLock:採用類似銀行排號叫好的方式實現自旋鎖的公平性,但是由於不停的讀取serviceNum,每次讀寫操作都必須在多個處理器快取之間進行快取同步,這會導致繁重的系統匯流排和記憶體的流量,大大降低系統整體的效能。
  • CLHLock和MCSLock通過連結串列的方式避免了減少了處理器快取同步,極大的提高了效能,區別在於CLHLock是通過輪詢其前驅節點的狀態,而MCS則是檢視當前節點的鎖狀態。
  • CLHLock在NUMA架構下使用會存在問題。在沒有cache的NUMA系統架構中,由於CLHLock是在當前節點的前一個節點上自旋,NUMA架構中處理器存取本地記憶體的速度高於通過網路存取其他節點的記憶體,所以CLHLock在NUMA架構上不是最優的自旋鎖。

到此這篇關於Golang自旋鎖的相關介紹的文章就介紹到這了,更多相關Golang自旋鎖內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com