一、sync 包簡介
在併發編程中,爲了解決競爭條件問題,Go 語言提供了 sync 標準包,它提供了基本的同步原語,例如互斥鎖、讀寫鎖等。
sync 包使用建議:
除了 Once 和 WaitGroup 類型之外,大多數類型旨在供低級庫程序使用。更高級別的同步最好用 channel 通道和通信來完成。
sync 包中類型:
- sync.Mutex 互斥鎖
- sync.RWMutex 讀寫鎖
- sync.WaitGroup 等待組(等待一組 goroutine 完成)
- sync.Map 併發 Map
- sync.Once 執行一次
- sync.Pool 對象池
- sync.Cond 條件變量
此外,sync 下還有一個包 atomic, 它提供了對數據的原子操作。
另外,Go 的擴展包也提供了信號量這種同步原語:
- x/sync/semaphore
二、sync.Mutex 互斥鎖
sync.Mutex
是一個互斥鎖,它的作用就是保護臨界區,確保同一時間只有一個 Go 協程進入臨界區。
什麼是臨界區?爲什麼有臨界區?
在併發編程中,有一部分程序被併發訪問,這個訪問可能是多個協程/線程修改這部分程序數據,這樣的操作會導致意想不到的結果,爲了不讓操作導致意外結果,怎麼辦?就需要把這部分程序保護起來,一次只允許一個協程/線程訪問這部分區域。需要被保護的這部分程序區域就叫臨界區。
防止多個協程/線程同時進入臨界區,修改程序數據。
互斥鎖就是一種可以保護臨界區資源方式。
互斥鎖其實是一種最特殊的信號量,這個"量"只有 0 和 1,所以也叫互斥量。互斥量的值爲 0 和 1,用來表示加鎖和解鎖。互斥鎖是一種獨佔鎖,即同一時間只能有一個協程持有鎖,其他協程必須等待。
互斥鎖使得同一時刻只有一個協程執行某段程序,其他協程等待該協程執行完在搶鎖後執行。
如上圖所示:g1 用互斥鎖保護臨界區,g2 在中間嘗試獲取鎖失敗,g1 離開臨界區釋放鎖,g2 獲取到鎖然後進行相應操作,操作完後釋放鎖離開臨界區。
第一次使用後不得複製 Mutex。
互斥鎖使用:
- 互斥鎖有兩個方法
Lock()
加鎖和Unlock()
解鎖,他們是成對出現。當一個協程對資源上鎖後,其他協程只能等待該協程解鎖之後,才能再次上鎖。 - 它還有一個
TryLock()
,go1.18 之後添加的。- 當一個 goroutine 調用此方法試圖獲取鎖時,如果這把鎖沒有被其他 goroutine 持有,那麼這個 goroutine 獲取鎖並返回 true;
- 如果這把鎖已經被其它 goroutine 持有,或正準備給某個喚醒的 gorouine,那麼請求鎖的 goroutine 直接返回 false,不會阻塞在方法調用上。
Lock()
代碼段(臨界區)
Unlock()
爲了防止上鎖後忘記釋放鎖,實際使用中用 defer 來釋放鎖。
例子:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var a = 0
var lock sync.Mutex
for i := 0; i < 100; i++ { // 併發 100 個goroutine
go func(id int) {
lock.Lock()
defer lock.Unlock()
a += 1
fmt.Printf("goroutine %d, a=%d\n", id, a)
}(i)
}
time.Sleep(time.Second) //等待1秒, 確保所有的協程執行完
}
三、sync.RWMutex 讀寫鎖
sync.RWMutex 讀寫鎖,對數據操進加鎖進一步細分,針對讀操作和寫操作分別進行加鎖和解鎖。
在讀寫鎖下,讀操作和讀操作之間不互斥,多個寫操作是互斥,讀操作和寫操作也是互斥。
- 當一個 goroutine 獲取讀鎖之後,其它的 goroutine 此時想獲取讀鎖,那麼可以繼續獲取鎖,不用等待解鎖;此時想獲取寫鎖,就會阻塞等待直到讀解鎖;
- 當一個 goroutine 獲取寫鎖之後,其它的 goroutine 無論是獲取讀鎖還是寫鎖,都會阻塞等待。
讀寫鎖的好處:
多個讀之間不互斥,讀鎖就可以降低對數據讀取加互斥鎖的性能損耗。而不像互斥鎖那樣對所有的數據操作,不管是讀還是寫,同等對待,都加一把大鎖處理。
在讀多寫少的場景下,更適合用讀寫鎖。
RWMutex 讀寫鎖的方法:
- Mutex 的加鎖和解鎖:Lock() 和 Unlock()
- 只讀加鎖和加鎖:RLock() 和 RUnlock()
- RLock() 加讀鎖時如果存在寫鎖,則不能加鎖;當只有讀鎖或無鎖時,可以加讀鎖,且讀鎖可以加載多個。
- RUnlock() 解讀鎖。沒有讀鎖情況下調用 RUlock() 會導致 panic。
釋放鎖用 defer 來釋放鎖
// 使用 RWMutex 的僞碼,當然正式代碼不會這樣寫,會用 defer 釋放鎖
mutex := sync.RWMutex{}
mutex.Lock()
// 操作的資源
mutex.Unlock()
mutex.RLock()
// 讀的資源
mutex.RUlock()
例子:
package main
import (
"fmt"
"sync"
"time"
)
var sum = 0
var rwMutex sync.RWMutex
func main() {
// 併發寫
for i := 1; i <= 50; i++ {
go writeSum()
}
// 併發讀
for i := 1; i <= 20; i++ {
go fmt.Println("readSum: ", readSum())
}
time.Sleep(time.Second * 2) // 防止主程序退出,子協程還沒運行完
fmt.Println("end sum: ", sum)
}
func writeSum() {
rwMutex.Lock() // 讀寫鎖
defer rwMutex.Unlock() // 釋放鎖
sum += 1
}
func readSum() int {
rwMutex.RLock() // 讀寫鎖加讀鎖
defer rwMutex.RUnlock() // 釋放讀鎖
return sum
}
四、sync.WaitGroup 等待組
sync.WaitGroup,等待一組或多個 goroutine 執行完成。
WaitGroup 內部有一個安全的計數器,它調用 Add(n int) 方法把計數器 +n;使用 Done() 方法,將計數器減 1,Done() 的底層是調用 Add(-1);調用 Wait() 方法等待所有的 goroutine 執行完,即計數器爲 0,Wait() 就返回。
WaitGroup 詳細原理,可以看我前面的文章:sync.WaitGroup源碼分析 。
- WaitGroup 裏的方法:
- Add(n),設置要等待的子 goroutine 數量,n 表示要等待數量
- Done(),子 goroutine 執行完後,計數器減一
- Wait(),阻塞等待所有子 goroutine 執行完
例子:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
fmt.Println("子 goroutine1")
}()
go func() {
defer wg.Done()
fmt.Println("子 goroutine2")
}()
wg.Wait() // 等待所有的子goroutine結束
fmt.Println("程序運行結束")
}
五、sync.Map 併發Map
在 Go 語言中,內置數據結構 map 並不是併發安全的,所以官方就出了一個 sync.Map。
希望瞭解 sync.Map 的原理,可以看這篇文章:深入理解Go語言(05):sync.map原理分析 。
sync.Map 裏的常用方法:
go v1.20.1
- Store(key, value any),設置鍵的值
- Load(key any) (value any, ok bool),獲取值
- Delete(key any),根據 key 刪除值
- LoadAndDelete(key any) (value any, loaded bool),根據 key 刪除值,返回以前的值如果還存在
- LoadOrStore(key, value any)(actual any, loaded bool),先根據 key 查找 value,如果找到則返回原來的值,loaded 爲 true;如果沒有找到 key 對應的 value 值,則存在 key,value 值並將存儲值返回,loaded 爲 false
- Range(f func(key, value any) bool),遍歷 sync.Map 的元素
更多方法請查看:https://pkg.go.dev/sync#Map
例子:
package main
import (
"fmt"
"sync"
)
func main() {
var syncmap sync.Map
syncmap.Store("li", 12)
syncmap.Store("han", "lu")
syncmap.Store("mei", 34)
fmt.Println(syncmap.Load("han"))
// key 不存在
val, ok := syncmap.LoadOrStore("lei", "lei")
fmt.Println(val, ok)
// key 存在
val, ok = syncmap.LoadOrStore("han", "cunzai")
fmt.Println(val, ok)
syncmap.Delete("mei")
syncmap.Range(func(k, v any) bool {
fmt.Println("k-v: ", k, v)
return true
})
}
參考
- https://pkg.go.dev/sync sync
- https://www.cnblogs.com/jiujuan/p/13365901.html sync.Map 原理