Go語(yǔ)言并發(fā)之Sync包的6個(gè)關(guān)鍵概念總結(jié)
1.sync.Mutex和sync.RWMutex
要知道,mutex(互斥)對(duì)于我們 gopher 來(lái)說就像一個(gè)老伙計(jì)。在處理 goroutine 時(shí),確保它們不會(huì)同時(shí)訪問資源是非常重要的,而 mutex 可以幫助我們做到這一點(diǎn)。
sync.Mutex
看看這個(gè)簡(jiǎn)單的例子,我沒有使用互斥鎖來(lái)保護(hù)我們的變量 a:
var a = 0
func Add() {
a++
}
func main() {
for i := 0; i < 500; i++ {
go Add()
}
time.Sleep(5 * time.Second)
fmt.Println(a)
}此代碼的結(jié)果是不可預(yù)測(cè)的。如果幸運(yùn)的話,您可能會(huì)得到 500,但通常結(jié)果會(huì)小于 500?,F(xiàn)在,讓我們使用互斥體增強(qiáng)我們的 Add 函數(shù):
var mtx = sync.Mutex{}
func Add() {
mtx.Lock()
defer mtx.Unlock()
a++
}現(xiàn)在,代碼提供了預(yù)期的結(jié)果。但是使用 sync.RWMutex 呢?
為什么使用 sync.RWMutex
想象一下,您正在檢查 a 變量,但其他 goroutines 也在調(diào)整它。您可能會(huì)得到過時(shí)的信息。那么,解決這個(gè)問題的方法是什么?
讓我們退后一步,使用我們的舊方法,將 sync.Mutex 添加到我們的 Get() 函數(shù)中:
func Add() {
mtx.Lock()
defer mtx.Unlock()
a++
}
func Get() int {
mtx.Lock()
defer mtx.Unlock()
return a
}但這里的問題是,如果您的服務(wù)或程序調(diào)用 Get() 數(shù)百萬(wàn)次而只調(diào)用 Add() 幾次,那么我們實(shí)際上是在浪費(fèi)資源,因?yàn)槲覀兇蟛糠謺r(shí)間甚至都沒有修改它而將所有內(nèi)容都鎖定了。
這就是 sync.RWMutex 突然出現(xiàn)來(lái)拯救我們的一天,這個(gè)聰明的小工具旨在幫助我們處理同時(shí)讀取和寫入的情況。
var mtx = sync.RWMutex{}
func Add() {
mtx.Lock()
defer mtx.Unlock()
a++
}
func Look() {
mtx.RLock()
defer mtx.RUnlock()
fmt.Println(a)
}那么,RWMutex 有什么了不起的呢?好吧,它允許數(shù)百萬(wàn)次并發(fā)讀取,同時(shí)確保一次只能進(jìn)行一次寫入。讓我澄清一下它是如何工作的:
- 寫入時(shí),讀取被鎖定。
- 讀取時(shí),寫入被鎖定。
- 多次讀取不會(huì)相互鎖定。
sync.Locker
哦對(duì)了,Mutex和RWMutex都實(shí)現(xiàn)了sync.Locker接口{},簽名是這樣的:
// A Locker represents an object that can be locked and unlocked.
type Locker interface {
Lock()
Unlock()
}如果你想創(chuàng)建一個(gè)接受 Locker 的函數(shù),你可以將這個(gè)函數(shù)與你的自定義 locker 或同步互斥鎖一起使用:
func Add(mtx sync.Locker) {
mtx.Lock()
defer mtx.Unlock()
a++
}2. sync.WaitGroup
您可能已經(jīng)注意到我使用了 time.Sleep(5 * time.Second) 來(lái)等待所有 goroutine 完成,但老實(shí)說,這是一個(gè)非常丑陋的解決方案。
這就是 sync.WaitGroup 出現(xiàn)的地方:
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 500; i++ {
wg.Add(1)
go func() {
defer wg.Done()
Add()
}()
}
wg.Wait()
fmt.Println(a)
}sync.WaitGroup 有 3 個(gè)主要方法:Add、Done 和 Wait。
首先是 Add(delta int):此方法將 WaitGroup 計(jì)數(shù)器增加 delta 的值。你通常會(huì)在生成 goroutine 之前調(diào)用它,表示有一個(gè)額外的任務(wù)需要完成。
如果我們將 WaitGroup 放在 go func() {} 中,您認(rèn)為會(huì)發(fā)生什么?
go func() {
wg.Add(1)
defer wg.Done()
Add()
}()我的編譯器喊道,“應(yīng)該在啟動(dòng) goroutine 之前調(diào)用 wg.Add(1) 以避免競(jìng)爭(zhēng)”,我的運(yùn)行時(shí)出現(xiàn)恐慌,“panic: sync: WaitGroup is reused before previous
Wait has returned”。
其他兩種方法非常簡(jiǎn)單:
當(dāng)一個(gè) goroutine 結(jié)束它的任務(wù)時(shí), Done 被調(diào)用。
Wait 會(huì)阻塞調(diào)用者,直到 WaitGroup 計(jì)數(shù)器歸零,這意味著所有派生的 goroutine 都已完成它們的任務(wù)。
3. sync.Once
假設(shè)您在一個(gè)包中有一個(gè) CreateInstance() 函數(shù),但您需要確保它在使用前已初始化。所以你在不同的地方多次調(diào)用它,你的實(shí)現(xiàn)看起來(lái)像這樣:
var i = 0
var _isInitialized = false
func CreateInstance() {
if _isInitialized {
return
}
i = GetISomewhere()
_isInitialized = true
}但是如果有多個(gè) goroutine 調(diào)用這個(gè)方法呢? i = GetISomeWhere 行會(huì)運(yùn)行多次,即使您為了穩(wěn)定性只希望它執(zhí)行一次。
您可以使用我們之前討論過的互斥鎖,但同步包提供了一種更方便的方法:sync.Once
var i = 0
var once = &sync.Once{}
func CreateInstance() {
once.Do(func() {
i = GetISomewhere()
})
}使用 sync.Once,你可以確保一個(gè)函數(shù)只執(zhí)行一次,不管它被調(diào)用了多少次或者有多少 goroutines 同時(shí)調(diào)用它。
4. sync.Pool
想象一下,你有一個(gè)池,里面有一堆你想反復(fù)使用的對(duì)象。這可以減輕垃圾收集器的一些壓力,尤其是在創(chuàng)建和銷毀這些資源的成本很高的情況下。
所以,無(wú)論何時(shí)你需要一個(gè)對(duì)象,你都可以從池中取出它。當(dāng)您使用完它時(shí),您可以將它放回池中以備日后重復(fù)使用。
var pool = sync.Pool{
New: func() interface{} {
return 0
},
}
func main() {
pool.Put(1)
pool.Put(2)
pool.Put(3)
a := pool.Get().(int)
b := pool.Get().(int)
c := pool.Get().(int)
fmt.Println(a, b, c) // Output: 1, 3, 2 (order may vary)
}請(qǐng)記住,將對(duì)象放入池中的順序不一定是它們出來(lái)的順序,即使多次運(yùn)行上述代碼時(shí)順序也是隨機(jī)。
讓我分享一些使用 sync.Pool 的技巧:
- 它非常適合長(zhǎng)期存在并且有多個(gè)實(shí)例需要管理的對(duì)象,例如數(shù)據(jù)庫(kù)連接(1000 個(gè)連接?)、worker goroutine,甚至緩沖區(qū)。
- 在將對(duì)象返回池之前始終重置對(duì)象的狀態(tài)。這樣,您可以避免任何無(wú)意的數(shù)據(jù)泄漏或奇怪的行為。
- 不要指望池中已經(jīng)存在的對(duì)象,因?yàn)樗鼈兛赡軙?huì)意外釋放。
5. sync.Map
當(dāng)您同時(shí)使用 map 時(shí),有點(diǎn)像使用 RWMutex。您可以同時(shí)進(jìn)行多次讀取,但不能進(jìn)行多次讀寫或?qū)懭?。如果存在沖突,您的服務(wù)將崩潰而不是覆蓋數(shù)據(jù)或?qū)е乱馔庑袨椤?/p>
這就是 sync.Map 派上用場(chǎng)的地方,因?yàn)樗梢詭椭覀儽苊膺@個(gè)問題。讓我們仔細(xì)看看 sync.Map 給我們提供什么:
- CompareAndDelete (go 1.20):如果值匹配則刪除鍵的條目;如果不存在值或舊值為 nil,則返回 false。
- CompareAndSwap(go 1.20):如果新舊值匹配,則交換一個(gè)鍵,只要確保舊值是可比較的。
- Swap (go 1.20):交換鍵的值并返回舊值(如果存在)。
- LoadOrStore:獲取當(dāng)前鍵值或保存并返回提供的值(如果不存在)
- Range (f func(key, value any):遍歷映射,將函數(shù) f 應(yīng)用于每個(gè)鍵值對(duì)。如果 f 說返回 false,它會(huì)停止。
- Store
- Delete
- Load
- LoadAndDelete
Q: 我們?yōu)槭裁床皇褂脦в?Mutex 的常規(guī) map 呢?
我通常選擇帶有 RWMutex 的 map,但在某些情況下認(rèn)識(shí)到 sync.Map 的強(qiáng)大功能很重要。那么,它真正發(fā)光的地方在哪里呢?
如果您有許多 goroutines 訪問 map 中的單獨(dú)鍵,則具有單個(gè)互斥鎖的常規(guī) map 可能會(huì)導(dǎo)致爭(zhēng)用,因?yàn)樗鼉H針對(duì)單個(gè)寫操作鎖定整個(gè) map。
另一方面,sync.Map 使用更完善的鎖定機(jī)制,有助于最大限度地減少此類場(chǎng)景中的爭(zhēng)用。
6. sync.Cond
將 sync.Cond 視為支持多個(gè) goroutine 等待和相互交互的條件變量。為了更好地理解,讓我們看看如何使用它。
首先,我們需要?jiǎng)?chuàng)建帶有 Locker 的 sync.Cond:
var mtx sync.Mutex var cond = sync.NewCond(&mtx)
goroutine 調(diào)用 cond.Wait 并等待來(lái)自其他地方的信號(hào)以繼續(xù)執(zhí)行:
func dummyGoroutine(id int) {
cond.L.Lock()
defer cond.L.Unlock()
fmt.Printf("Goroutine %d is waiting...\n", id)
cond.Wait()
fmt.Printf("Goroutine %d received the signal.\n", id)
}然后,另一個(gè) goroutine(就像主 goroutine)調(diào)用 cond.Signal(),讓我們等待的 goroutine 繼續(xù):
func main() {
go dummyGoroutine(1)
time.Sleep(1 * time.Second)
fmt.Println("Sending signal...")
cond.Signal()
time.Sleep(1 * time.Second)
}結(jié)果如下所示:
Goroutine 1 is waiting...
Sending signal...
Goroutine 1 received the signal.
如果有多個(gè) goroutines 在等待我們的信號(hào)怎么辦?這就是我們可以使用廣播的時(shí)候:
func main() {
go dummyGoroutine(1)
go dummyGoroutine(2)
time.Sleep(1 * time.Second)
cond.Broadcast() // broadcast to all goroutines
time.Sleep(1 * time.Second)
}結(jié)果如下所示:
Goroutine 1 is waiting...
Goroutine 2 is waiting...
Goroutine 2 received the signal.
Goroutine 1 received the signal.
到此這篇關(guān)于Go語(yǔ)言并發(fā)之Sync包的6個(gè)關(guān)鍵概念總結(jié)的文章就介紹到這了,更多相關(guān)Go語(yǔ)言Sync包內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang設(shè)計(jì)模式之外觀模式的實(shí)現(xiàn)
這篇文章主要介紹了Golang設(shè)計(jì)模式之外觀模式的實(shí)現(xiàn),外觀模式是一種常用的設(shè)計(jì)模式之一,是一種結(jié)構(gòu)型設(shè)計(jì)模式,它提供了一個(gè)簡(jiǎn)單的接口來(lái)訪問復(fù)雜系統(tǒng)的各種功能,從而降低了系統(tǒng)的復(fù)雜度,需要詳細(xì)了解可以參考下文2023-05-05
在 Golang 中實(shí)現(xiàn)一個(gè)簡(jiǎn)單的Http中間件過程詳解
本文在go web中簡(jiǎn)單的實(shí)現(xiàn)了中間件的機(jī)制,這樣帶來(lái)的好處也是顯而易見的,當(dāng)然社區(qū)也有一些成熟的 middleware 組件,包括 Gin 一些Web框架中也包含了 middleware 相關(guān)的功能,具體內(nèi)容詳情跟隨小編一起看看吧2021-07-07
Go語(yǔ)言使用組合的思想實(shí)現(xiàn)繼承
這篇文章主要為大家詳細(xì)介紹了在 Go 里面如何使用組合的思想實(shí)現(xiàn)“繼承”,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)Go語(yǔ)言有一定的幫助,需要的可以了解一下2022-12-12
詳解Golang?ProtoBuf的基本語(yǔ)法總結(jié)
最近項(xiàng)目是采用微服務(wù)架構(gòu)開發(fā)的,各服務(wù)之間通過gPRC調(diào)用,基于ProtoBuf序列化協(xié)議進(jìn)行數(shù)據(jù)通信,因此接觸學(xué)習(xí)了Protobuf,本文會(huì)對(duì)Protobuf的語(yǔ)法做下總結(jié),感興趣的小伙伴們可以參考借鑒,希望對(duì)大家能有所幫助2022-10-10

