首頁 > 軟體

初識Golang Mutex互斥鎖的使用

2022-10-10 14:00:25

前言

在學習作業系統的時候,我們應該都學習過臨界區、互斥鎖這些概念,用於在並行環境下保證狀態的正確性。比如在秒殺時,100 個使用者同時搶 10 個電腦,為了避免少賣或者超賣,就需要使用鎖來進行並行控制。在 Go語言 裡面互斥鎖是 sync.Mutex ,我們本篇文章就來學習下為什麼要使用互斥鎖、如何使用互斥鎖,以及使用時的常見問題。

為什麼要使用互斥鎖

我們來看一個範例:我們起了 10000 個協程將變數 num 加1,因此肯定會存在並行,如果我們不控制並行,10000 個協程都執行完後,該變數的值很大概率不等於 10000。

那麼為什麼會出現這個問題呢,原因是 num++ 不是原子操作,它會先讀取變數 num 當前值,然後對這個值 加1,再把結果儲存到 num 中。例如 10 個 goroutine 同時執行到 num++ 這一行,可能同時讀取 num=1000,都加1後再儲存, num=1001,這就與想要的結果不符。

package main

import (
 "fmt"
 "sync"
)

func main() {
 num := 0

 var wg sync.WaitGroup
 threadCount := 10000
 wg.Add(threadCount)
  
 for i := 0; i < threadCount; i++ {
  go func() {
   defer wg.Done()
   num++
  }()
 }
  
 wg.Wait() // 等待 10000 個協程都執行完
  fmt.Println(num) // 9388(每次都可能不一樣)

}

我們如果使用了互斥鎖,可以保證每次進入臨界區的只有一個 goroutine,一個 goroutine 執行完後,另一個 goroutine 才能進入臨界區執行,最終就實現了並行控制。

並行獲取鎖示意圖

package main

import (
 "fmt"
 "sync"
)

func main() {
 num := 0
 var mutex sync.Mutex  // 互斥鎖

 var wg sync.WaitGroup
 threadCount := 10000
 wg.Add(threadCount)
 for i := 0; i < threadCount; i++ {
  go func() {
   defer wg.Done()
   
   mutex.Lock() // 加鎖
   num++ // 臨界區
   mutex.Unlock() // 解鎖
   
  }()
 }

 wg.Wait()
 fmt.Println(num) // 10000

}

如何使用互斥鎖

Mutex 保持 Go 一貫的簡潔風格,開箱即用,宣告一個變數預設是沒有加鎖的,加鎖使用 Lock() 方法,解鎖使用 Unlock() 方法。

使用方式一:直接宣告使用

這個在上例中已經體現了,直接看上面的例子就好

使用方式二:封裝在其他結構體中

我們可以將 Mutex 封裝在 struct 中,封裝成執行緒安全的函數供外部呼叫。比如我們封裝了一個執行緒安全的計數器,呼叫 Add() 就加一,呼叫Count() 返回計數器的值。

package main

import (
 "fmt"
 "sync"
)


type Counter struct {
 num   int
 mutex sync.Mutex
}

// 加一操作,涉及到臨界區 num,加鎖解鎖
func (counter *Counter) Add() {
 counter.mutex.Lock()
 defer counter.mutex.Unlock()
 counter.num++
}

// 返回數量,涉及到臨界區 num,加鎖解鎖
func (counter *Counter) Count() int {
 counter.mutex.Lock()
 defer counter.mutex.Unlock()
 return counter.num
}

func main() {
 threadCount := 10000
  
 var counter Counter
 var wg sync.WaitGroup
 
 wg.Add(threadCount)
 for i := 0; i < threadCount; i++ {
  go func() {
   defer wg.Done()
   counter.Add()
  }()
 }

 wg.Wait() // 等待所有 goroutine 都執行完
 fmt.Println(counter.Count()) // 10000

}

在 Go 中,map 結構是不支援並行的,如果並行讀寫就會 panic

// 執行會 panic,提示 fatal error: concurrent map writes
func main() {
 m := make(map[string]string)
 var wait sync.WaitGroup
 wait.Add(1000)

 for i := 0; i < 1000; i++ {
  item := fmt.Sprintf("%d", i)
  go func() {
   wait.Done()
   m[item] = item
  }()
 }
 wait.Wait()
}

基於 Mutex ,我們可以實現一個執行緒安全的 map

import (
 "fmt"
 "sync"
)

type ConcurrentMap struct {
 mutex sync.Mutex
 items map[string]interface{}
}

func (c *ConcurrentMap) Add(key string, value interface{}) {
 c.mutex.Lock()
 defer c.mutex.Unlock()
 c.items[key] = value
}

func (c *ConcurrentMap) Remove(key string) {
 c.mutex.Lock()
 defer c.mutex.Unlock()
 delete(c.items, key)
}
func (c *ConcurrentMap) Get(key string) interface{} {
 c.mutex.Lock()
 defer c.mutex.Unlock()
 return c.items[key]
}

func NewConcurrentMap() ConcurrentMap {
 return ConcurrentMap{
  items: make(map[string]interface{}),
 }
}

func main() {
 m := NewConcurrentMap()
 var wait sync.WaitGroup
 wait.Add(1000)

 for i := 0; i < 1000; i++ {
  item := fmt.Sprintf("%d", i)
  go func() {
   wait.Done()
   m.Add(item, item)
  }()
 }
 wait.Wait()
 fmt.Println(m.Get("100")) // 100
}

當然,基於互斥鎖 Mutex 實現的執行緒安全 map 並不是效能最好的,基於讀寫鎖 sync.RWMutex 和 分片 可以實現效能更好的、執行緒安全的 map,開發中比較常用的並行安全 map 是 orcaman / concurrent-map(https://github.com/orcaman/concurrent-map)。

互斥鎖的常見問題

從上面可以看出,Mutex 的使用過程方法比較簡單,但還是有幾點需要注意:

1.Mutex是可以在 goroutine A 中加鎖,在 goroutine B 中解鎖的,但是在實際使用中,儘量保證在同一個 goroutine 中加解鎖。比如 goroutine A 申請到了鎖,在處理臨界區資源的時候,goroutine B 把鎖釋放了,但是 A 以為自己還持有鎖,會繼續處理臨界區資源,就可能會出現問題。

2.Mutex的加鎖解鎖基本都是成對出現,為了解決忘記解鎖,可以使用 defer 語句,在加鎖後直接 defer mutex.Unlock();但是如果處理完臨界區資源後還有很多耗時操作,為了儘早釋放鎖,不建議使用 defer,而是在處理完臨界區資源後就呼叫 mutex.Unlock() 儘早釋放鎖。

// 邏輯複雜,可能會忘記釋放鎖
func main() {
 var mutex sync.Mutex
 mutex.Lock()

 if *** {
  if *** {
   // 處理臨界區資源
   mutex.Unlock()
   return
  }
  // 處理臨界區資源
  mutex.Unlock()
  return
 }

 // 處理臨界區資源
 mutex.Unlock()
 return
}


// 避免邏輯複雜忘記釋放鎖,使用 defer語句,成對出現
func main() {
 var mutex sync.Mutex
 mutex.Lock()
 defer mutex.Unlock()

 if *** {
  if *** {
   // 處理臨界區資源
   return
  }
  // 處理臨界區資源
  return
 }

 // 處理臨界區資源
 return
}

3.Mutex 不能複製使用

Mutex 是有狀態的,比如我們對一個 Mutex 加鎖後,再進行復制操作,會把當前的加鎖狀態也給複製過去,基於加鎖的 Mutex 再加鎖肯定不會成功。進行復制操作可能聽起來是一個比較低階的錯誤,但是無意間可能就會犯這種錯誤。

package main

import (
 "fmt"
 "sync"
)

type Counter struct {
 mutex sync.Mutex
 num   int
}

func SomeFunc(c Counter) {
 c.mutex.Lock()
 defer c.mutex.Unlock()
 c.num--
}

func main() {
 var counter Counter
 counter.mutex.Lock()
 defer counter.mutex.Unlock()

 counter.num++
 // Go都是值傳遞,這裡複製了 counter,此時 counter.mutex 是加鎖狀態,在 SomeFunc 無法再次加鎖,就會一直等待
 SomeFunc(counter)

}

以上就是初識Golang Mutex互斥鎖的使用的詳細內容,更多關於Golang Mutex互斥鎖的資料請關注it145.com其它相關文章!


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