Golang的鎖機制使用及說明
golang中的鎖分為互斥鎖、讀寫鎖、原子鎖即原子操作。
在 Golang 里有專門的方法來實現鎖,就是 sync 包,這個包有兩個很重要的鎖類型。一個叫 Mutex, 利用它可以實現互斥鎖。
一個叫 RWMutex,利用它可以實現讀寫鎖。
- 全局鎖 sync.Mutex,是同一時刻某一資源只能上一個鎖,此鎖具有排他性,上鎖后只能被此線程使用,直至解鎖。加鎖后即不能讀也不能寫。全局鎖是互斥鎖,即 sync.Mutex 是個互斥鎖。
- 讀寫鎖 sync.RWMutex ,將使用者分為讀者和寫者兩個概念,支持同時多個讀者一起讀共享資源,但寫時只能有一個,并且在寫時不可以讀。理論上來說,sync.RWMutex 的 Lock() 也是個互斥鎖。
踩坑點
將上面的結論展開一下,更清晰得說(為避免理解偏差寧可嘮叨一些):
- sync.Mutex 的鎖是不可以嵌套使用的。
- sync.RWMutex 的 mu.Lock() 是不可以嵌套的。
- sync.RWMutex 的 mu.Lock() 中不可以嵌套 mu.RLock()。(這是個注意的地方)
否則,會 panic fatal error: all goroutines are asleep - deadlock!
var l sync.RWMutex ? func lockAndRead() { // 可讀鎖內使用可讀鎖 ?? ?l.RLock() ?? ?defer l.RUnlock() ? ?? ?l.RLock() ?? ?defer l.RUnlock() } ? func main() { ?? ?lockAndRead() ?? ?time.Sleep(5 * time.Second) }
而將 lockAndRead 換為以下三種函數均會造成 panic:
func lockAndRead1() { // 全局鎖內使用全局鎖 ?? ?l.Lock() ?? ?defer l.Unlock() ? ?? ?l.Lock() ?? ?defer l.Unlock() } ? func lockAndRead2() { // 全局鎖內使用可讀鎖 ?? ?l.Lock() ?? ?defer l.Unlock() // 由于 defer 是棧式執(zhí)行,所以這兩個鎖是嵌套結構 ? ?? ?l.RLock() ?? ?defer l.RUnlock() } ? func lockAndRead3() { // 可讀鎖內使用全局鎖 ?? ?l.RLock() ?? ?defer l.RUnlock() ? ?? ?l.Lock() ?? ?defer l.Unlock() }
互斥鎖 Mutex
互斥鎖有兩個方法:加鎖、解鎖。
一個互斥鎖只能同時被一個 goroutine 鎖定,其它 goroutine 將阻塞直到互斥鎖被解鎖(重新爭搶對互斥鎖的鎖定)。
使用Lock加鎖后,不能再進行加鎖,只有當對其進行Unlock解鎖之后,才能對其加鎖。這個很好理解。
- 如果對一個未加鎖的資源進行解鎖,會引發(fā)panic異常。
- 可以在一個goroutine中對一個資源加鎖,而在另外一個goroutine中對該資源進行解鎖。
- 不要在持有鎖的時候做 IO 操作。盡量只通過持有鎖來保護 IO 操作需要的資源而不是 IO 操作本身
func (m *Mutex) Lock() func (m *Mutex) Unlock()
讀寫鎖 RWMutex
讀寫鎖有四個方法:讀的加鎖、解鎖,寫的加鎖、解鎖。
func ?(*RWMutex)Lock() func (*RWMutex)Unlock()
和
func (*RWMutex)RLock() func (*RWMutex)RUnlock()
RWMutex的使用主要事項
- 1、讀鎖的時候無需等待讀鎖的結束
- 2、讀鎖的時候要等待寫鎖的結束
- 3、寫鎖的時候要等待讀鎖的結束
- 4、寫鎖的時候要等待寫鎖的結束
謹防鎖拷貝
type MyMutex struct { ?? ?count int ?? ?sync.Mutex } ? func main() { ?? ?var mu MyMutex ?? ?mu.Lock() ?? ?var mu1 = mu ?? ?mu.count++ ?? ?mu.Unlock() ?? ?mu1.Lock() ?? ?mu1.count++ ?? ?mu1.Unlock() ?? ?fmt.Println(mu.count, mu1.count) }
加鎖后復制變量,會將鎖的狀態(tài)也復制,所以 mu1 其實是已經加鎖狀態(tài),再加鎖會死鎖
查看數據競爭
加上 -race 參數驗證數據競爭
以下代碼有什么問題,怎么解決?
func main() { ?? ?total, sum := 0, 0 ?? ?for i := 1; i <= 10; i++ { ?? ??? ?sum += i ?? ??? ?go func() { ?? ??? ??? ?total += i ?? ??? ?}() ?? ?} ?? ?fmt.Printf("total:%d sum %d", total, sum) }
該題的第二個考點:data race。
因為存在多 goroutine 同時寫 total 變量的問題,所以有數據競爭。
可以加上 -race 參數驗證
go run -race main.go ================== WARNING: DATA RACE Read at 0x00c0001b4020 by goroutine 8: ? main.main.func1() ? ? ? /Users/xuxinhua/main.go:12 +0x57 ? Previous write at 0x00c0001b4020 by main goroutine: ? main.main() ? ? ? /Users/xuxinhua/main.go:9 +0x10b ? Goroutine 8 (running) created at: ? main.main() ? ? ? /Users/xuxinhua/main.go:11 +0xe7 ==================
正確答案
package main ? import ( ? ? "sync/atomic" ? ? "sync" ? ? "fmt" ) ? func main() { ? ? var wg sync.WaitGroup ? ? var total int64 ? ? sum := 0 ? ? for i := 1; i <= 10; i++ { ? ? ? ? wg.Add(1) ? ? ? ? sum += i ? ? ? ? go func(i int) { ? ? ? ? ? ? defer wg.Done() ? ? ? ? ? ? atomic.AddInt64(&total, int64(i)) ? ? ? ? }(i) ? ? } ? ? wg.Wait() ? ? ? fmt.Printf("total:%d sum %d", total, sum) }
總結
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
Go語言之重要數組類型切片(slice)make,append函數解讀
這篇文章主要介紹了Go語言之重要數組類型切片(slice)make,append函數用法,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-09-09Golang自定義開發(fā)Prometheus?exporter詳解
Exporter是基于Prometheus實施的監(jiān)控系統(tǒng)中重要的組成部分,承擔數據指標的采集工作,這篇文章主要為大家介紹了如何自定義編寫開發(fā)?Prometheus?exporter,感興趣的可以了解一下2023-06-06