互斥鎖與讀寫互斥鎖的妙用
概述
在併發編程中,控制共享資源的訪問是至關重要的。
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