互斥鎖與讀寫互斥鎖的妙用

概述

在併發編程中,控制共享資源的訪問是至關重要的。

Go 語言提供了兩種主要的互斥鎖,即 sync.Mutex(互斥鎖)和 sync.RWMutex(讀寫互斥鎖)。

本文將討論這兩種鎖的使用方式、原理和適用場景,並通過實例代碼演示它們在併發環境中的正確應用。

1. sync.Mutex(互斥鎖)

1.1 互斥鎖基本使用

互斥鎖用於保護共享資源,確保同一時刻只有一個 Goroutine 可以訪問。

下面是一個簡單的示例

package main
import (
  "fmt"
  "sync"
  "time"
)
var counter int
var mutex sync.Mutex
func main() {
  var wg sync.WaitGroup
  for i := 0; i < 3; i++ {
    wg.Add(1)
    go incrementCounter(i, &wg)
  }
  wg.Wait()
  fmt.Printf("Final Counter: %d\n", counter)
}
func incrementCounter(id int, wg *sync.WaitGroup) {
  defer wg.Done()
  for i := 0; i < 5; i++ {
    mutex.Lock()
    counter++
    fmt.Printf("Goroutine %d: Counter = %d\n", id, counter)
    mutex.Unlock()
    time.Sleep(100 * time.Millisecond)
  }
}

在這個用例中,使用 sync.Mutex 對 counter 變量進行了保護。

每次修改 counter 前,通過 Lock 方法鎖定互斥鎖,修改完成後使用 Unlock 方法釋放鎖。

這樣可以確保多個 Goroutine 同時訪問時不會發生數據競爭。

1.2 避免死鎖

在使用互斥鎖時,要特別注意避免死鎖。

例如,如果一個 Goroutine 在鎖定互斥鎖後忘記釋放鎖,那麼其他 Goroutine 將永遠無法獲取該鎖。

下面是一個簡單的死鎖示例

package main
import (
  "sync"
  "time"
)
var mutex sync.Mutex
func main() {
  var wg sync.WaitGroup
  wg.Add(1)
  go deadlockExample(&wg)
  // 此處故意不調用 wg.Done(),導致死鎖
  wg.Wait()
}
func deadlockExample(wg *sync.WaitGroup) {
  defer wg.Done()
  mutex.Lock()
  defer mutex.Unlock()
  // 此處故意不釋放鎖,導致死鎖
  time.Sleep(2 * time.Second)
}

在這個示例中,deadlockExample Goroutine 獲取了互斥鎖,

並在 defer 語句中註冊了 Unlock 操作,但由於未調用 wg.Done(),

導致 main Goroutine 永遠無法結束,形成死鎖。

2. sync.RWMutex(讀寫互斥鎖)

2.1 讀寫互斥鎖基本使用

讀寫互斥鎖相比於互斥鎖,更加靈活,允許多個 Goroutine 同時獲取讀鎖,但只允許一個 Goroutine 獲取寫鎖。

用一個簡單的示例要演示

package main
import (
  "fmt"
  "sync"
  "time"
)
var data map[string]string
var rwMutex sync.RWMutex
func main() {
  data = make(map[string]string)
  var wg sync.WaitGroup
  for i := 0; i < 3; i++ {
    wg.Add(1)
    go readData(i, &wg)
  }
  for i := 0; i < 2; i++ {
    wg.Add(1)
    go writeData(i, &wg)
  }
  wg.Wait()
}
func readData(id int, wg *sync.WaitGroup) {
  defer wg.Done()
  rwMutex.RLock()
  defer rwMutex.RUnlock()
  fmt.Printf("Goroutine %d reading data: %v\n", id, data)
  time.Sleep(500 * time.Millisecond)
}
func writeData(id int, wg *sync.WaitGroup) {
  defer wg.Done()
  rwMutex.Lock()
  defer rwMutex.Unlock()
  key := fmt.Sprintf("key%d", id)
  value := fmt.Sprintf("value%d", id)
  data[key] = value
  fmt.Printf("Goroutine %d writing data: %v\n", id, data)
  time.Sleep(500 * time.Millisecond)
}

在這個示例中,用 sync.RWMutex 對 data 進行讀寫保護。

多個 Goroutine 可以同時獲取讀鎖,但只有一個 Goroutine 能夠獲取寫鎖。

這樣可以在讀多寫少的場景中提高併發性能。

2.2 避免寫鎖飢餓

寫鎖飢餓是指當有讀鎖持有時,寫鎖一直無法獲取的情況。

要避免寫鎖飢餓,應該儘量減小讀操作的臨界區,避免長時間佔用讀鎖。

下面是一個示例演示

package main
import (
  "fmt"
  "sync"
  "time"
)
var data map[string]string
var rwMutex sync.RWMutex
func main() {
  data = make(map[string]string)
  var wg sync.WaitGroup
  for i := 0; i < 3; i++ {
    wg.Add(1)
    go readData(i, &wg)
  }
    //  等待讀鎖獲取
  time.Sleep(100 * time.Millisecond)
  wg.Add(1)
  go writeData(1, &wg)
  wg.Wait()
}
func readData(id int, wg *sync.WaitGroup) {
  defer wg.Done()
  rwMutex.RLock()
  defer rwMutex.RUnlock()
  fmt.Printf("Goroutine %d reading data: %v\n", id, data)
  time.Sleep(500 * time.Millisecond)
}
func writeData(id int, wg *sync.WaitGroup) {
  defer wg.Done()
  rwMutex.Lock()
  defer rwMutex.Unlock()
  key := fmt.Sprintf("key%d", id)
  value := fmt.Sprintf("value%d", id)
  data[key] = value
  fmt.Printf("Goroutine %d writing data: %v\n", id, data)
  time.Sleep(500 * time.Millisecond)
}

在這個示例中,讓三個 Goroutine 同時獲取讀鎖,然後等待一段時間後再嘗試獲取寫鎖。

這樣可以模擬寫鎖飢餓的情況。

總結

通過本文的詳細講解和實例演示,瞭解了 Go 語言中互斥鎖和讀寫互斥鎖的使用方式、原理以及注意事項。

在併發編程中,選擇合適的鎖機制是確保程序正確性和性能的關鍵一步。

通過靈活運用 sync.Mutex 和 sync.RWMutex,可以更好地處理共享資源的併發訪問,提高程序的健壯性和性能。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/BpAdlIhBeZgsJs2kyNNN8Q