go的鎖包——sync
上一篇文章介紹了各種鎖的基本概念,這篇文章主要學習一下Go的標準庫sync包,提供了基本的同步基元.
要注意的是,sync除了Once和WaitGroup類型,大部分都是適用於低水平程序線程,高水平的同步使用channel通信更好一些
互斥鎖——Mutex
-
互斥鎖: 同一時刻只能有一個讀或寫的場景
var mu sync.Mutex func main() { mu.Lock() // 使用defer釋放鎖的話會比顯示調用Unlock成本高 defer mu.Unlock() }
-
Mutex實現
// A Mutex is a mutual exclusion lock. // The zero value for a Mutex is an unlocked mutex. // // A Mutex must not be copied after first use. type Mutex struct { state int32 sema uint32 }
-
可以看到Mutex的結構主要是
-
state: 表示鎖當前的狀態,零值表示未上鎖,通過state來進行鎖的計數
-
sema: 信號量,實現排隊…通過pv操作從等待隊列中阻塞/喚醒goroutinue,等待鎖的goroutine會掛到等待隊列中,並且陷入睡眠不被調度,unlock鎖時才喚醒。
-
-
詳細的圖解解析可以參考這兩篇文章:
-
-
一個互斥鎖只能同時被一個goroutine鎖定,其他go程將被阻塞直到互斥鎖被解鎖,重新爭奪互斥鎖
-
要注意的是: Go中沒有重入鎖, 對一個已經上鎖的Mutex再次上鎖會導致程序死鎖
讀寫鎖——RWMutes
-
讀寫鎖/共享鎖: 允許有多個讀鎖,但只能有一個寫鎖,讀寫鎖可以分別對讀,寫進行鎖定,一般用於“多讀少寫”場景
var rw sync.RWMutex func main() { rw.Lock() // 對寫操作進行鎖定 rw.Unlock() // 對寫操作進行解鎖 rw.RLock() // 對讀操作進行鎖定 rw.RUnlock() // 對讀操作進行解鎖 }
-
寫鎖權限高於讀鎖, 寫鎖會優先鎖定,當有寫鎖時沒辦法獲得讀鎖, 當只有讀鎖或無鎖時可以獲取多個讀鎖
-
源碼解析可以看這篇文章:Golang 讀寫鎖RWMutex
單次執行——Once
-
使用once可以保證只運行一次,是協程安全的
var once sync.Once func main(){ once.Do( func(){ } ) }
-
Once的實現分析:
-
Once結構
// Once is an object that will perform exactly one action. type Once struct { // done indicates whether the action has been performed. // It is first in the struct because it is used in the hot path. // The hot path is inlined at every call site. // Placing done first allows more compact instructions on some architectures (amd64/x86), // and fewer instructions (to calculate offset) on other architectures. done uint32 m Mutex }
- Done : 計數器,統計執行次數
- m: 互斥鎖
-
Do
func (o *Once) Do(f func()) { // Note: Here is an incorrect implementation of Do: // // if atomic.CompareAndSwapUint32(&o.done, 0, 1) { // f() // } // // Do guarantees that when it returns, f has finished. // This implementation would not implement that guarantee: // given two simultaneous calls, the winner of the cas would // call f, and the second would return immediately, without // waiting for the first's call to f to complete. // This is why the slow path falls back to a mutex, and why // the atomic.StoreUint32 must be delayed until after f returns. if atomic.LoadUint32(&o.done) == 0 { // Outlined slow-path to allow inlining of the fast-path. o.doSlow(f) } } func (o *Once) doSlow(f func()) { o.m.Lock() defer o.m.Unlock() if o.done == 0 { defer atomic.StoreUint32(&o.done, 1) f() } }
- 當done的次數爲0的時候執行doSlow
- 執行後defer將done的值置爲1
- 使用原子操作避免加鎖提高了性能
-
Once實現單例模式
package singleton import ( "fmt" "sync" ) // 全局實例 type singleton struct { data int } var sing *singleton // 小寫私有實例變量 var once sync.Once // 保證線程安全,只執行一次 func GetInstance(num int) *singleton { once.Do(func() { sing = &singleton{data:num} fmt.Println("實例對象的值爲和地址爲:", sing, &sing) }) return sing }
-
等待組——WaitGroup
-
用於等待一組協程的結束,可以優雅的避免用sleep或for等待.
// 1.創建子協程先調用Add增加等待計數 // 2.子協程結束後調用Done減少協程計數 // 3.主協程中調用Wait方法進行等待,直到計數器歸零繼續執行 package main import ( "fmt" "sync" "time" ) func main() { var wg sync.WaitGroup wg.Add(1) go func() { time.Sleep(time.Second * 3) // 子協程結束了等待組就調用done wg.Done() fmt.Println("子協程1結束了") }() wg.Wait() fmt.Println("main over") }
條件等待——Cond
-
條件等待是不同協程各用一個鎖, 互斥鎖是不同協程公用一個鎖
-
條件等待不是像互斥鎖那樣來保護臨界區和共享資源的,是用於協調想要訪問共享資源的那些線程,維護一個等待隊列,在共享資源的狀態發生變化時,可以用來通知被互斥鎖所阻塞的線程
package main import ( "fmt" "sync" "time" ) /** * 條件等待 */ func main() { var wg sync.WaitGroup cond := sync.NewCond(&sync.Mutex{}) for i := 0; i < 10; i++ { wg.Add(1) go func(i int) { cond.L.Lock() defer cond.L.Unlock() // Wait()等待通知: 阻塞當前線程,直到收到該條件變量發來的通知 cond.Wait() wg.Done() // do other fmt.Println(i) }(i) } fmt.Println("正被阻塞。。。") time.Sleep(time.Second * 1) // Signal()單發通知: 讓該條件變量向至少一個正在等待它的通知的線程發送通知,表示共享數據的狀態已經改變。 cond.Signal() fmt.Println("通知已被釋放") time.Sleep(time.Second * 1) fmt.Println("廣播") // Broadcast廣播通知: 讓條件變量給正在等待它的通知的所有線程都發送通知。 cond.Broadcast() wg.Wait() } // 正被阻塞。。。 // 通知已被釋放 // 4 // 廣播 // 9 // 3 // 0 // 1 // 2 // 5 // 8 // 7 // 6
協程安全的Map——Sync.Map
-
協程安全的map的特性:
- Sync.Map無須初始化,直接聲明即可, 不能使用make創建
- Sync.Map 不能使用 map 的方式進行取值和設置等操作,而是使用 sync.Map 的方法進行調用,Store 表示存儲,Load 表示獲取,Delete 表示刪除。
- Sync.Map的鍵和值以interface{}類型進行保存
- 使用 Range 配合一個回調函數進行遍歷操作,通過回調函數返回內部遍歷出來的值,Range 參數中回調函數的返回值在需要繼續迭代遍歷時,返回 true,終止迭代遍歷時,返回 false
- Sync.Map沒有獲取map數量的方法,可以在Range自行計算
-
package main import ( "fmt" "sync" ) func main() { var scene sync.Map // 將鍵值對保存到sync.Map scene.Store("key", "value") // 從sync.Map中根據鍵取值 fmt.Println(scene.Load("key")) // 根據鍵刪除對應的鍵值對 scene.Delete("key") // 遍歷所有sync.Map中的鍵值對 scene.Range(func(k, v interface{}) bool { fmt.Println("iterate:", k, v) return true }) }
臨時對象池——Pool
Go是自動垃圾回收,在高性能場景下,不能任意產生太多垃圾,會造成gc負擔重
解決辦法: 使用pool來保存和複用臨時對象,減少內存分配和gc壓力
-
Pool是一個可以分別存取的臨時對象的集合,是協程安全的,可以緩存申請但未使用的item用於之後的重用來減少GC的壓力
-
Pool不能被複制
- Get: 從池中選擇任意一個item,刪除池中的引用計數並提供給調用者,如果沒有取得item會返回new的結果
- Put: 放入池中
package main import ( "fmt" "runtime" "sync" ) /** * 臨時對象池 */ func main() { pool := sync.Pool{New: func() interface{} { return 0 }} pool.Put(1) a := pool.Get() fmt.Println(a) pool.Put(1) runtime.GC() // gc並不會釋放池 b := pool.Get() fmt.Println(b) } // 1 // 1
-
爲什麼不叫cache而叫pool, 更多源碼的可以參考go夜讀的這個視頻