一文帶你深入理解Go語言中的sync.Cond
在 go 的標(biāo)準(zhǔn)庫中,提供了 sync.Cond
這個(gè)并發(fā)原語,讓我們可以實(shí)現(xiàn)多個(gè) goroutine
等待某一條件滿足之后再繼續(xù)執(zhí)行。 它需要配合 sync.Mutex
一起使用,因?yàn)?Cond
的 Wait
方法需要在 Mutex
的保護(hù)下才能正常工作。 對(duì)于條件變量,可能大多數(shù)人只是知道它的存在,但是用到它的估計(jì)寥寥無幾,因?yàn)楹芏嗖l(fā)場景的處理都能使用 chan
來實(shí)現(xiàn), 而且 chan
的使用也更加簡單。 但是在某些場景下,Cond
可能是最好的選擇,本文就來探討一下 Cond
的使用場景,基本用法,以及它的實(shí)現(xiàn)原理。
sync.Cond 是什么
sync.Cond
表示的是條件變量,它是一種同步機(jī)制,用來協(xié)調(diào)多個(gè) goroutine
之間的同步,當(dāng)共享資源的狀態(tài)發(fā)生變化的時(shí)候, 可以通過條件變量來通知所有等待的 goroutine
去重新獲取共享資源。
適用場景
在實(shí)際使用中,我們可能會(huì)有多個(gè) goroutine
在執(zhí)行的過程中,由于某一條件不滿足而阻塞的情況。 這個(gè)時(shí)候,我們就可以使用條件變量來實(shí)現(xiàn) goroutine
之間的同步。比如,我們有一個(gè) goroutine
用來獲取數(shù)據(jù), 但是可能會(huì)比較耗時(shí),這個(gè)時(shí)候,我們就可以使用條件變量來實(shí)現(xiàn) goroutine
之間的同步, 當(dāng)數(shù)據(jù)準(zhǔn)備好之后,就可以通過條件變量來通知所有等待的 goroutine
去重新獲取共享資源。
sync.Cond
條件變量用來協(xié)調(diào)想要訪問共享資源的那些 goroutine
,當(dāng)共享資源的狀態(tài)發(fā)生變化的時(shí)候, 它可以用來通知所有等待的 goroutine
去重新獲取共享資源。
sync.Cond 的基本用法
sync.Cond
的基本用法非常簡單,我們只需要通過 sync.NewCond
方法來創(chuàng)建一個(gè) Cond
實(shí)例, 然后通過 Wait
方法來等待條件滿足,通過 Signal
或者 Broadcast
方法來通知所有等待的 goroutine
去重新獲取共享資源。
NewCond 創(chuàng)建實(shí)例
sync.NewCond
方法用來創(chuàng)建一個(gè) Cond
實(shí)例,它的參數(shù)是一個(gè) Locker
接口,我們可以傳入一個(gè) Mutex
或者 RWMutex
實(shí)例。 這個(gè)條件變量的 Locker
接口就是用來保護(hù)共享資源的。
Wait 等待條件滿足
Wait
方法用來等待條件滿足,它會(huì)先釋放 Cond
的鎖(Cond.L
),然后阻塞當(dāng)前 goroutine
(實(shí)際調(diào)用的是 goparkunlock
),直到被 Signal
或者 Broadcast
喚醒。
它做了如下幾件事情:
- 釋放
Cond
的鎖(Cond.L
),然后阻塞當(dāng)前goroutine
。(所以,使用之前需要先鎖定) - 被
Signal
或者Broadcast
喚醒之后,會(huì)重新獲取Cond
的鎖(Cond.L
)。 - 之后,就返回到
goroutine
阻塞的地方繼續(xù)執(zhí)行。
Signal 通知一個(gè)等待的 goroutine
Signal
方法用來通知一個(gè)等待的 goroutine
,它會(huì)喚醒一個(gè)等待的 goroutine
,然后繼續(xù)執(zhí)行當(dāng)前 goroutine
。 如果沒有等待的 goroutine
,則不會(huì)有任何操作。
Broadcast 通知所有等待的 goroutine
Broadcast
方法用來通知所有等待的 goroutine
,它會(huì)喚醒所有等待的 goroutine
,然后繼續(xù)執(zhí)行當(dāng)前 goroutine
。 如果沒有等待的 goroutine
,則不會(huì)有任何操作。
sync.Cond 使用實(shí)例
下面我們通過一個(gè)實(shí)例來看一下 sync.Cond
的使用方法。
package cond import ( "fmt" "sync" "testing" "time" ) var done bool var data string func write(c *sync.Cond) { fmt.Println("writing.") // 讓 reader 先獲取鎖,模擬條件不滿足然后 wait 的情況 time.Sleep(time.Millisecond * 10) c.L.Lock() // 模擬耗時(shí)的寫操作 time.Sleep(time.Millisecond * 50) data = "hello world" done = true fmt.Println("writing done.") c.L.Unlock() c.Broadcast() } func read(c *sync.Cond) { fmt.Println("reading") c.L.Lock() for !done { fmt.Println("reader wait.") c.Wait() } fmt.Println("read done.") fmt.Println("data:", data) defer c.L.Unlock() } func TestCond(t *testing.T) { var c = sync.NewCond(&sync.Mutex{}) go read(c) // 讀操作 go read(c) // 讀操作 go write(c) // 寫操作 time.Sleep(time.Millisecond * 100) // 等待操作完成 }
輸出:
reading
reader wait. // 還沒獲取完數(shù)據(jù),需要等待
writing.
reading
reader wait.
writing done. // 獲取完數(shù)據(jù)了,通知所有等待的 reader
read done. // 讀取到數(shù)據(jù)了
data: hello world // 輸出讀取到的數(shù)據(jù)
read done.
data: hello world
這個(gè)例子可以粗略地用下圖來表示:
說明:
read1
和reader2
表示兩個(gè)goroutine
,它們都會(huì)調(diào)用read
函數(shù)。- 在
done
為false
的時(shí)候,reader1
和reader2
都會(huì)調(diào)用c.Wait()
函數(shù),然后阻塞等待。 write
表示一個(gè)goroutine
,它會(huì)調(diào)用write
函數(shù)。- 在
write
函數(shù)中,獲取完數(shù)據(jù)之后,會(huì)將done
設(shè)置為true
,然后調(diào)用c.Broadcast()
函數(shù),通知所有等待的reader
去重新獲取共享資源。 reader1
和reader2
在解除阻塞狀態(tài)后,都會(huì)重新獲取共享資源,然后輸出讀取到的數(shù)據(jù)。
在這個(gè)例子中,done
的功能是標(biāo)記,用來表示共享資源是否已經(jīng)獲取完畢,如果沒有獲取完畢,那么 reader
就會(huì)阻塞等待。
為什么要用 sync.Cond
在文章開頭,我們說了,很多并發(fā)編程的問題都可以通過 channel
來解決。 同樣的,在上面提到的 sync.Cond
的使用場景,使用 channel
也是可以實(shí)現(xiàn)的, 我們只要 close(ch)
來關(guān)閉 channel
就可以實(shí)現(xiàn)通知多個(gè)等待的協(xié)程了。
那么為什么還要用 sync.Cond
呢? 主要原因是,sync.Cond
可以重復(fù)地進(jìn)行 Wait()
和 Signal()
、Broadcast()
操作, 但是,如果想通過關(guān)閉 chan
來實(shí)現(xiàn)這個(gè)功能的話,那就只能通知一次了。 因?yàn)?channel
只能關(guān)閉一次,關(guān)閉一個(gè)已經(jīng)關(guān)閉的 channel
會(huì)導(dǎo)致程序 panic。
使用 channel
的另外一種方式是,記錄 reader
的數(shù)量,然后通過往 channel
中發(fā)送多次數(shù)據(jù)來實(shí)現(xiàn)通知多個(gè) reader
。 但是這樣一來代碼就會(huì)復(fù)雜很多,從另一個(gè)角度說,出錯(cuò)的概率大了很多。
close channel 廣播實(shí)例
下面的例子模擬了使用 close(chan)
來實(shí)現(xiàn) sync.Cond
中那種廣播功能,但是只能通知一次。
package close_chan import ( "fmt" "testing" "time" ) var data string func read(c <-chan struct{}) { fmt.Println("reading.") // 從 chan 接收數(shù)據(jù),如果 chan 中沒有數(shù)據(jù),會(huì)阻塞。 // 如果能接收到數(shù)據(jù),或者 chan 被關(guān)閉,會(huì)解除阻塞狀態(tài)。 <-c fmt.Println("data:", data) } func write(c chan struct{}) { fmt.Println("writing.") // 模擬耗時(shí)的寫操作 time.Sleep(time.Millisecond * 10) data = "hello world" fmt.Println("write done.") // 關(guān)閉 chan 的時(shí)候,會(huì)通知所有的 reader // 所有等待從 chan 接收數(shù)據(jù)的 goroutine 都會(huì)被喚醒 close(c) } func TestCloseChan(t *testing.T) { ch := make(chan struct{}) go read(ch) go read(ch) go write(ch) // 不能關(guān)閉已經(jīng)關(guān)閉的 chan time.Sleep(time.Millisecond * 20) // panic: close of closed channel // 下面這行代碼會(huì)導(dǎo)致 panic //go write(ch) time.Sleep(time.Millisecond * 100) }
輸出:
writing.
reading. // 會(huì)阻塞直到寫完
reading. // 會(huì)阻塞直到寫完
write done. // 寫完之后,才能讀
data: hello world
data: hello world
上面例子的 write
不能多次調(diào)用,否則會(huì)導(dǎo)致 panic。
sync.Cond 基本原理
go 的 sync.Cond
中維護(hù)了一個(gè)鏈表,這個(gè)鏈表記錄了所有阻塞的 goroutine
,也就是由于調(diào)用了 Wait
而阻塞的 goroutine
。 而 Signal
和 Broadcast
方法就是用來喚醒這個(gè)鏈表中的 goroutine
的。 Signal
方法只會(huì)喚醒鏈表中的第一個(gè) goroutine
,而 Broadcast
方法會(huì)喚醒鏈表中的所有 goroutine
。
下圖是 Signal
方法的效果,可以看到,Signal
方法只會(huì)喚醒鏈表中的第一個(gè) goroutine
:
說明:
notifyList
是sync.Cond
中維護(hù)的一個(gè)鏈表,這個(gè)鏈表記錄了所有阻塞的goroutine
。head
是鏈表的頭節(jié)點(diǎn),tail
是鏈表的尾節(jié)點(diǎn)。Signal
方法只會(huì)喚醒鏈表中的第一個(gè)goroutine
。
而 Broadcast
方法會(huì)喚醒 notifyList
中的所有 goroutine
。
sync.Cond 的設(shè)計(jì)與實(shí)現(xiàn)
最后,我們來看一下 sync.Cond
的設(shè)計(jì)與實(shí)現(xiàn)。
sync.Cond 模型
sync.Cond
的模型如下所示:
type Cond struct { noCopy noCopy // L is held while observing or changing the condition L Locker // L 在觀察或改變條件時(shí)被持有 notify notifyList checker copyChecker }
屬性說明:
noCopy
是一個(gè)空結(jié)構(gòu)體,用來檢查sync.Cond
是否被復(fù)制。(在編譯前通過go vet
命令來檢查)L
是一個(gè)Locker
接口,用來保護(hù)條件變量。notify
是一個(gè)notifyList
類型,用來記錄所有阻塞的goroutine
。checker
是一個(gè)copyChecker
類型,用來檢查sync.Cond
是否被復(fù)制。(如果在運(yùn)行時(shí)被復(fù)制,會(huì)導(dǎo)致panic
)
notifyList 結(jié)構(gòu)體
notifyList
是 sync.Cond
中維護(hù)的一個(gè)鏈表,這個(gè)鏈表記錄了所有因?yàn)楣蚕碣Y源還沒準(zhǔn)備好而阻塞的 goroutine
。它的定義如下所示:
type notifyList struct { wait atomic.Uint32 notify uint32 // 阻塞的 waiter 名單。 lock mutex // 鎖 head *sudog // 阻塞的 goroutine 鏈表(鏈表頭) tail *sudog // 阻塞的 goroutine 鏈表(鏈表尾) }
屬性說明:
wait
是下一個(gè)waiter
的編號(hào)。它在鎖外自動(dòng)遞增。notify
是下一個(gè)要通知的waiter
的編號(hào)。它可以在鎖外讀取,但只能在持有鎖的情況下寫入。lock
是一個(gè)mutex
類型,用來保護(hù)notifyList
。head
是一個(gè)sudog
類型,用來記錄阻塞的goroutine
鏈表的頭節(jié)點(diǎn)。tail
是一個(gè)sudog
類型,用來記錄阻塞的goroutine
鏈表的尾節(jié)點(diǎn)。
notifyList
的方法說明:
notifyList
中包含了幾個(gè)操作阻塞的 goroutine
鏈表的方法。
notifyListAdd
方法將waiter
的編號(hào)加 1。notifyListWait
方法將當(dāng)前的goroutine
加入到notifyList
中。(也就是將當(dāng)前協(xié)程掛起)notifyListNotifyOne
方法將notifyList
中的第一個(gè)goroutine
喚醒。notifyListNotifyAll
方法將notifyList
中的所有goroutine
喚醒。notifyListCheck
方法檢查 notifyList 的大小是否正確。
sync.Cond 的方法
notifyList
就不細(xì)說了,本文重點(diǎn)講解一下 sync.Cond
的實(shí)現(xiàn)。
Wait 方法
Wait
方法用在當(dāng)條件不滿足的時(shí)候,將當(dāng)前運(yùn)行的協(xié)程掛起。
func (c *Cond) Wait() { // 檢查是否被復(fù)制 c.checker.check() // 更新 notifyList 中需要等待的 waiter 的數(shù)量 // 返回當(dāng)前需要插入 notifyList 的編號(hào) t := runtime_notifyListAdd(&c.notify) // 解鎖 c.L.Unlock() // 掛起當(dāng)前 g,直到被喚醒 runtime_notifyListWait(&c.notify, t) // 喚醒之后,重新加鎖。 // 因?yàn)樽枞敖怄i了。 c.L.Lock() }
對(duì)于 Wait
方法,我們需要注意的是,使用之前,我們需要先調(diào)用 L.Lock()
方法加鎖,然后再調(diào)用 Wait
方法,否則會(huì)報(bào)錯(cuò)。
文檔里面的例子:
c.L.Lock() for !condition() { c.Wait() } // ...使用條件... // 這里是我們?cè)跅l件滿足之后,需要執(zhí)行的代碼。 c.L.Unlock()
好了,問題來了,調(diào)用 Wait
方法之前為什么要先加鎖呢?
這是因?yàn)樵谖覀兪褂霉蚕碣Y源的時(shí)候,可能一些代碼是互斥的,所以我們需要加鎖。 這樣我們就可以保證在我們使用共享資源的時(shí)候,不會(huì)被其他協(xié)程修改。 但是如果因?yàn)闂l件不滿足,我們需要等待的話,我們不可能在持有鎖的情況下等待, 因?yàn)樵谛薷臈l件的時(shí)候,可能也需要加鎖,這樣就會(huì)造成死鎖。
另外一個(gè)問題是,為什么要使用 for
來檢查條件是否滿足,而不是使用 if
呢?
這是因?yàn)樵谖覀冋{(diào)用 Wait
方法之后,可能會(huì)有其他協(xié)程喚醒我們,但是條件并沒有滿足, 這個(gè)時(shí)候依然是需要繼續(xù) Wait
的。
Signal 方法
Signal
方法用在當(dāng)條件滿足的時(shí)候,將 notifyList
中的第一個(gè) goroutine
喚醒。
func (c *Cond) Signal() { // 檢查 sync.Cond 是否被復(fù)制了 c.checker.check() // 喚醒 notifyList 中的第一個(gè) goroutine runtime_notifyListNotifyOne(&c.notify) }
Broadcast 方法
Broadcast
方法用在當(dāng)條件滿足的時(shí)候,將 notifyList
中的所有 goroutine
喚醒。
func (c *Cond) Broadcast() { // 檢查 sync.Cond 是否被復(fù)制了 c.checker.check() // 喚醒 notifyList 中的所有 goroutine runtime_notifyListNotifyAll(&c.notify) }
copyChecker 結(jié)構(gòu)體
copyChecker
結(jié)構(gòu)體用來檢查 sync.Cond
是否被復(fù)制。它實(shí)際上只是一個(gè) uintptr
類型的值。
type copyChecker uintptr // check 方法檢查 copyChecker 是否被復(fù)制了。 func (c *copyChecker) check() { if uintptr(*c) != uintptr(unsafe.Pointer(c)) && !atomic.CompareAndSwapUintptr((*uintptr)(c), 0, uintptr(unsafe.Pointer(c))) && uintptr(*c) != uintptr(unsafe.Pointer(c)) { panic("sync.Cond is copied") } }
copyChecker
的值只有兩種可能:
0
,表示還沒有調(diào)用過Wait
,Signal
或Broadcast
方法。uintptr(unsafe.Pointer(©Checker))
,表示已經(jīng)調(diào)用過Wait
,Signal
或Broadcast
方法。在這幾個(gè)方法里面會(huì)調(diào)用check
方法,所以copyChecker
的值會(huì)被修改。
所以如果 copyChecker
的值不是 0
,也不是 uintptr(unsafe.Pointer(©Checker))
(也就是最初的 copyChecker
的內(nèi)存地址),則表示 copyChecker
被復(fù)制了。
需要注意的是,這個(gè)方法在調(diào)用 CompareAndSwapUintptr
還會(huì)檢查一下,這是因?yàn)橛锌赡軙?huì)并發(fā)調(diào)用 CompareAndSwapUintptr
, 如果另外一個(gè)協(xié)程調(diào)用了 CompareAndSwapUintptr
并且成功了,那么當(dāng)前協(xié)程的這個(gè) CompareAndSwapUintptr
調(diào)用會(huì)返回 false
, 這個(gè)時(shí)候就需要檢查是否是因?yàn)榱硗庖粋€(gè)協(xié)程調(diào)用了 CompareAndSwapUintptr
而導(dǎo)致的,如果是的話,就不會(huì) panic
。
為什么 sync.Cond 不能被復(fù)制
從上一小節(jié)中我們可以看到,sync.Cond
其實(shí)是不允許被復(fù)制的,但是如果是在調(diào)用 Wait
, Signal
或 Broadcast
方法之前復(fù)制,那倒是沒關(guān)系。
這是因?yàn)?sync.Cond
中維護(hù)了一個(gè)阻塞的 goroutine
列表。如果 sync.Cond
被復(fù)制了,那么這個(gè)列表就會(huì)被復(fù)制,這樣就會(huì)導(dǎo)致兩個(gè) sync.Cond
都包含了這個(gè)列表;但是我們喚醒的時(shí)候,只會(huì)有其中一個(gè) sync.Cond
被喚醒,另外一個(gè) sync.Cond
就會(huì)一直阻塞。 所以 go 直接從語言層面限制了這種情況,不允許 sync.Cond
被復(fù)制。
總結(jié)
sync.Cond
是一個(gè)條件變量,它可以用來協(xié)調(diào)多個(gè) goroutine
之間的同步,當(dāng)條件滿足的時(shí)候,去通知那些因?yàn)闂l件不滿足被阻塞的 goroutine
繼續(xù)執(zhí)行。
sync.Cond
的接口比較簡單,只有 Wait
, Signal
和 Broadcast
三個(gè)方法。
Wait
方法用來阻塞當(dāng)前goroutine
,直到條件滿足。調(diào)用Wait
方法之前,需要先調(diào)用L.Lock
方法加鎖。Signal
方法用來喚醒notifyList
中的第一個(gè)goroutine
。Broadcast
方法用來喚醒notifyList
中的所有goroutine
。
sync.Cond
的實(shí)現(xiàn)也比較簡單,它的核心就是 notifyList
,它是一個(gè)鏈表,用來保存所有因?yàn)闂l件不滿足而被阻塞的 goroutine
。
用關(guān)閉 channel
的方式也可以實(shí)現(xiàn)類似的廣播功能,但是有個(gè)問題是 channel
不能被重復(fù)關(guān)閉,所以這種方式無法被多次使用。也就是說使用這種方式無法多次廣播。
使用 channel
發(fā)送通知的方式也是可以的,但是這樣實(shí)現(xiàn)起來就復(fù)雜很多了,就更容易出錯(cuò)了。
sync.Cond
中使用 copyChecker
來檢查 sync.Cond
是否被復(fù)制,如果被復(fù)制了,就會(huì) panic
。需要注意的是,這里的復(fù)制是指調(diào)用了 Wait
,Signal
或 Broadcast
方法之后,sync.Cond
被復(fù)制了。在調(diào)用這幾個(gè)方法之前進(jìn)行復(fù)制是沒有影響的。
以上就是一文帶你深入理解Go語言中的sync.Cond的詳細(xì)內(nèi)容,更多關(guān)于Go語言 sync.Cond的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go實(shí)現(xiàn)HTTP請(qǐng)求轉(zhuǎn)發(fā)的示例代碼
請(qǐng)求轉(zhuǎn)發(fā)是一項(xiàng)核心且常見的功能,本文主要介紹了Go實(shí)現(xiàn)HTTP請(qǐng)求轉(zhuǎn)發(fā)的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-05-05golang文件服務(wù)器的兩種方式(可以訪問任何目錄)
這篇文章主要介紹了golang文件服務(wù)器的兩種方式,可以訪問任何目錄,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-04-04Golang線上內(nèi)存爆掉問題排查(pprof)與解決
這篇文章主要介紹了Golang線上內(nèi)存爆掉問題排查(pprof)與解決,涉及到數(shù)據(jù)敏感,文中代碼是我模擬線上故障的一個(gè)情況,好在我們程序都有添加pprof監(jiān)控,于是直接通過go tool pprof分析,需要的朋友可以參考下2024-04-04詳解Go并發(fā)編程時(shí)如何避免發(fā)生競態(tài)條件和數(shù)據(jù)競爭
大家都知道,Go是一種支持并發(fā)編程的編程語言,但并發(fā)編程也是比較復(fù)雜和容易出錯(cuò)的。比如本篇分享的問題:競態(tài)條件和數(shù)據(jù)競爭的問題2023-04-04一文帶你搞懂Golang依賴注入的設(shè)計(jì)與實(shí)現(xiàn)
在現(xiàn)代的 web 框架里面,基本都有實(shí)現(xiàn)了依賴注入的功能,可以讓我們很方便地對(duì)應(yīng)用的依賴進(jìn)行管理。今天我們來看看 go 里面實(shí)現(xiàn)依賴注入的一種方式,感興趣的可以了解一下2023-01-01