Go語言提供了豐富的同步原語來處理并發編程中的共享資源訪問問題。其中最基礎也最常用的就是互斥鎖(Mutex)和讀寫鎖(RWMutex)。
1. sync.Mutex(互斥鎖)
Mutex核心特性
- 互斥性/排他性:同一時刻只有一個goroutine能持有鎖
- 不可重入:同一個goroutine重復加鎖會導致死鎖
- 零值可用:
sync.Mutex
的零值就是未鎖定的互斥鎖 - 非公平鎖:不保證goroutine獲取鎖的順序
Mutex例子
例1:
package mainimport ("fmt""math/rand""sync""time"
)var wait sync.WaitGroup
var count = 0var lock sync.Mutexfunc main() {wait.Add(10)for i := 0; i < 10; i++ {go func(data *int) {// 加鎖lock.Lock()// 模擬訪問耗時time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))// 訪問數據temp := *data// 模擬計算耗時time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))ans := 1// 修改數據*data = temp + ans// 解鎖lock.Unlock()fmt.Println(*data)wait.Done()}(&count)}wait.Wait()fmt.Println("最終結果", count)
}
輸出:
1
2
3
4
5
6
7
8
9
10
最終結果 10
解讀:
- lock 是一個互斥鎖,用于確保在任何時刻只有一個 goroutine 可以訪問和修改 count 變量,防止數據競爭。
- 每個 goroutine 首先通過 lock.Lock() 加鎖,確保在同一時間只有一個 goroutine 可以修改 count。
- time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000))) 模擬了對數據的訪問和計算耗時,這里的隨機數生成器用于在每次循環中生成一個 0 到 999 之間的隨機整數,作為睡眠的時間
- wait.Wait() 阻塞主 goroutine,直到等待組中的所有 goroutine 都完成任務。
- fmt.Println("最終結果", count) 打印 count 的最終值。
在 Go 語言中,func(data *int) 這樣的寫法是用來定義一個匿名函數,并且該匿名函數接受一個參數,參數類型是指向整型的指針。在這段代碼的目的是在并發環境中對一個共享變量 count 進行修改,以避免數據競爭。
- go func(data *int) { ... }(&count) 這里的 go 關鍵字用于啟動一個新的 goroutine。
- func(data *int) { ... } 是一個匿名函數,它接受一個參數 data,這個參數是一個指向整型的指針。
- (&count) 表示傳遞給匿名函數的參數是 count 變量的地址。通過傳遞指針,匿名函數可以直接訪問和修改 count 的值。
例2
package mainimport ("fmt""sync""time"
)var (counter intlock sync.Mutex
)func main() {var wg sync.WaitGroupfor i := 0; i < 10; i++ {wg.Add(1)go increment(&wg)}wg.Wait()fmt.Println("Final counter:", counter)
}func increment(wg *sync.WaitGroup) {defer wg.Done()lock.Lock() // 加鎖defer lock.Unlock() // 使用defer確保解鎖// 臨界區temp := countertime.Sleep(1 * time.Millisecond)counter = temp + 1
}
輸出:
Final counter: 10
解讀:
- var wg sync.WaitGroup:聲明了一個等待組wg,用于等待所有goroutine完成。
- wg.Add(1):為每次循環增加一個等待計數。
- go increment(&wg):啟動goroutine運行increment函數,并傳入等待組的地址。
- wg.Wait():等待所有等待計數為零,即所有goroutine完成。
- defer wg.Done():使用defer關鍵字確保函數執行完畢后調用wg.Done(),減少等待組的一個計數。
- lock.Lock():在函數執行前加鎖,防止多個goroutine同時訪問counter。
- defer lock.Unlock():同樣使用defer關鍵字確保函數執行完畢后解鎖。
- 臨界區代碼段:將counter的值賦給temp,休眠1毫秒,然后將counter設置為temp + 1。這里通過休眠模擬了一個耗時操作。
2. sync.RWMutex(讀寫鎖)
Go 中讀寫互斥鎖的實現是?sync.RWMutex
,它也同樣實現了?Locker
?接口,但它提供了更多可用的方法,如下:
// 加讀鎖
func (rw *RWMutex) RLock()// 非阻塞地嘗試加讀鎖 (Go 1.18+)
func (rw *RWMutex) TryRLock() bool// 解讀鎖
func (rw *RWMutex) RUnlock()// 加寫鎖
func (rw *RWMutex) Lock()// 非阻塞地嘗試加寫鎖 (Go 1.18+)
func (rw *RWMutex) TryLock() bool// 解寫鎖
func (rw *RWMutex) Unlock()
1. RWMutex基本概念
讀寫鎖的特點
- 并發讀:多個goroutine可以同時持有讀鎖
- 互斥寫:寫鎖是排他的,同一時間只能有一個goroutine持有寫鎖
- 寫優先:當有寫鎖等待時,新的讀鎖請求會被阻塞,防止寫鎖饑餓
與Mutex的區別
特性 | Mutex | RWMutex |
---|---|---|
并發讀 | 不支持 | 支持多個goroutine同時讀 |
并發寫 | 不支持 | 不支持 |
性能 | 一般 | 讀多寫少場景性能更好 |
復雜度 | 簡單 | 相對復雜 |
2. RWMutex的工作原理
鎖狀態
- 當寫鎖被持有時:所有讀鎖和寫鎖請求都會被阻塞
- 當讀鎖被持有時:新的讀鎖請求可以立即獲得鎖,寫鎖請求會被阻塞
- 當寫鎖請求等待時:新的讀鎖請求會被阻塞(寫優先)
內部實現要點
- 讀者計數:記錄當前持有讀鎖的goroutine數量
- 寫者標記:標識是否有goroutine持有或等待寫鎖
- 寫者信號量:用于喚醒等待的寫者
- 讀者信號量:用于喚醒等待的讀者
3. RWMutex的例子
線程安全的緩存實現
type Cache struct {mu sync.RWMutexitems map[string]interface{}
}func (c *Cache) Get(key string) (interface{}, bool) {c.mu.RLock()defer c.mu.RUnlock()item, found := c.items[key]return item, found
}func (c *Cache) Set(key string, value interface{}) {c.mu.Lock()defer c.mu.Unlock()c.items[key] = value
}func (c *Cache) Delete(key string) {c.mu.Lock()defer c.mu.Unlock()delete(c.items, key)
}
解讀
Cache 結構體
- items:一個映射(map),鍵為字符串,值為接口類型(interface{}),用于存儲緩存數據。
- mu:一個sync.RWMutex實例,用于控制對items的并發訪問。
Get 方法:
- c.mu.RLock():獲取讀鎖,允許多個讀協程同時訪問items。
- defer c.mu.RUnlock():確保在函數返回前釋放讀鎖。
- item, found := c.items[key]:從items中獲取指定key對應的值,并判斷該key是否存在。
- return item, found:返回獲取的值和是否找到的布爾值。
Set 方法:
- c.mu.Lock():獲取寫鎖,確保只有一個寫協程可以訪問items。
- defer c.mu.Unlock():確保在函數返回前釋放寫鎖。
- c.items[key] = value:將指定key對應的值設置為value。
Delete 方法:
- c.mu.Lock():獲取寫鎖,確保只有一個寫協程可以訪問items。
- defer c.mu.Unlock():確保在函數返回前釋放寫鎖。
- delete(c.items, key):從items中刪除指定key對應的鍵值對。
3.互斥鎖和讀寫鎖的區別和應用場景
核心區別對比
特性 | 互斥鎖(Mutex) | 讀寫鎖(RWMutex) |
---|---|---|
并發讀 | 完全互斥,讀操作也需要獨占鎖 | 允許多個goroutine同時持有讀鎖 |
并發寫 | 互斥,同一時間只有一個寫操作 | 互斥,同一時間只有一個寫操作 |
鎖類型 | 單一鎖類型 | 區分讀鎖(RLock)和寫鎖(Lock) |
性能開銷 | 較高(所有操作都互斥) | 讀操作開銷低,寫操作開銷與Mutex相當 |
實現復雜度 | 簡單 | 相對復雜 |
適用場景 | 讀寫操作頻率相當或寫多讀少 | 讀操作遠多于寫操作的場景 |
選擇場景
優先考慮RWMutex當:
- 讀操作次數是寫操作的5倍以上
- 讀操作臨界區較大(耗時較長)
- 需要支持高頻并發讀取
選擇Mutex當:
- 讀寫操作頻率相當(寫操作占比超過20%)
- 臨界區非常小(幾個CPU周期就能完成)
- 代碼簡單性比極致性能更重要
- 需要鎖升級/降級(雖然Go不支持,但Mutex更不容易出錯)
特殊考慮:
- 對于極高性能場景,可考慮
atomic
原子操作 - 對于復雜場景,可考慮
sync.Map
或分片鎖
- 對于極高性能場景,可考慮