Go中多協(xié)程協(xié)作之Sync.Cond
1. 程序中的通信方式
GO語言中有句名言:“不要用共享內(nèi)存來通信,而是使用通信來共享內(nèi)存”。
編程語言中,通信方式分為進程間通信、線程間通信。
1.進程間通信,常用方式:
- 有名管道
- 無名管道
- 信號
- 共享內(nèi)存
- 消息隊列
- 信號燈集
- socket
2.線程間通信,常用方式:
- 信號量
- 互斥鎖
- 條件變量
對于Go語言來說,Go程序啟動之后對外是一個進程,內(nèi)部包含若干協(xié)程,協(xié)程相當(dāng)于用戶態(tài)輕量級線程,所以協(xié)程的通信方式大多可以使用線程間通信方式來完成。
協(xié)程間通信方式,官方推薦使用channel,channel在一對一的協(xié)程之間進行數(shù)據(jù)交換與通信十分便捷。但是,一對多的廣播場景中,則顯得有點無力,此時就需要sync.Cond來輔助。
2. 什么是廣播?
舉個例子,上高中時,宿管老師每天早晨需要叫醒學(xué)生們?nèi)ド险n。這個時候,有兩種解決方法:①一個寢室一個寢室的把學(xué)生叫醒。②在宿舍樓安裝個廣播,到起床時間時,在廣播上叫醒學(xué)生。顯然,使用廣播的方式效率更高。
編程中的廣播可以理解為:多個操作流程依賴于一個操作流程完成后才能進行某種動作,這個被依賴的操作流程在喚醒所有依賴者時使用的一種通知方式。
在Go語言中,則可以使用sync.Cond來實現(xiàn)多個協(xié)程之間的廣播通知功能。
3. sync.Cond
cond是sync包下面的一種數(shù)據(jù)類型,相當(dāng)于線程間通信的條件變量方式。
- // Cond implements a condition variable, a rendezvous point
- // for goroutines waiting for or announcing the occurrence
- // of an event.
- //
- // Each Cond has an associated Locker L (often a *Mutex or *RWMutex),
- // which must be held when changing the condition and
- // when calling the Wait method.
- //
- // A Cond must not be copied after first use.
- type Cond struct {
- noCopy noCopy // 在第一次使用后不可復(fù)制,使用go vet作為檢測使用
- // L is held while observing or changing the condition
- // 根據(jù)需求初始化不同的鎖,如*Mutex 和 *RWMutex。注意是 指針類型
- L Locker
- // 具有頭尾指針的鏈表。存儲被阻塞的協(xié)程,通知時操作該鏈表中的協(xié)程
- notify notifyList
- checker copyChecker // 復(fù)制檢查,檢查cond實例是否被復(fù)制
- }
該數(shù)據(jù)類型提供的方法有:
- type Cond
- func NewCond(l Locker) *Cond
- func (c *Cond) Broadcast() // 通知所有協(xié)程,廣播
- func (c *Cond) Signal() // 通知一個協(xié)程
- func (c *Cond) Wait() // 阻塞等待,直到被喚醒
對應(yīng)源碼追溯
- // Wait atomically unlocks c.L and suspends execution
- // of the calling goroutine. After later resuming execution,
- // Wait locks c.L before returning. Unlike in other systems,
- // Wait cannot return unless awoken by Broadcast or Signal.
- //
- // Because c.L is not locked when Wait first resumes, the caller
- // typically cannot assume that the condition is true when
- // Wait returns. Instead, the caller should Wait in a loop:
- //
- // 注意下面的寫法是官方推薦的
- // c.L.Lock()
- // for !condition() {
- // c.Wait()
- // }
- // ... make use of condition ...
- // c.L.Unlock()
- //
- func (c *Cond) Wait() {
- // 檢查c是否是被復(fù)制的,如果是就panic
- c.checker.check()
- // 獲取等待隊列的一個ticket數(shù)值,作為喚醒時的一個令牌憑證
- t := runtime_notifyListAdd(&c.notify)
- // 解鎖
- c.L.Unlock()
- // 注意,上面的ticket數(shù)值會作為阻塞攜程的一個標識
- // 加入通知隊列里面
- // 到這里執(zhí)行g(shù)opark(),當(dāng)前協(xié)程掛起,直到signal或broadcast發(fā)起通知
- runtime_notifyListWait(&c.notify, t)
- // 被喚醒之后,先獲取鎖
- c.L.Lock()
- }
- // Signal wakes one goroutine waiting on c, if there is any.
- //
- // It is allowed but not required for the caller to hold c.L
- // during the call.
- func (c *Cond) Signal() {
- c.checker.check()
- runtime_notifyListNotifyOne(&c.notify) // 隨機挑選一個進行通知,wait阻塞解除
- }
- // Broadcast wakes all goroutines waiting on c.
- //
- // It is allowed but not required for the caller to hold c.L
- // during the call.
- func (c *Cond) Broadcast() {
- c.checker.check()
- // 通知所有阻塞等待的協(xié)程
- // 主要是喚醒 cond.notify 鏈表上的各個協(xié)程
- runtime_notifyListNotifyAll(&c.notify)
- }
使用方法,代碼示例:
- var locker sync.Mutex
- var cond = sync.NewCond(&locker)
- // NewCond(l Locker)里面定義的是一個接口,擁有l(wèi)ock和unlock方法。
- // 看到sync.Mutex的方法,func (m *Mutex) Lock(),可以看到是指針有這兩個方法,所以應(yīng)該傳遞的是指針
- func main() {
- // 啟動多個協(xié)程
- for i := 0; i < 10; i++ {
- gofunc(x int) {
- cond.L.Lock() // 獲取鎖
- defer cond.L.Unlock() // 釋放鎖
- cond.Wait() // 等待通知,阻塞當(dāng)前 goroutine
- // 通知到來的時候, cond.Wait()就會結(jié)束阻塞, do something. 這里僅打印
- fmt.Println(x)
- }(i)
- }
- time.Sleep(time.Second * 1) // 睡眠 1 秒,等待所有 goroutine 進入 Wait 阻塞狀態(tài)
- fmt.Println("Signal...")
- cond.Signal() // 1 秒后下發(fā)一個通知給已經(jīng)獲取鎖的 goroutine
- time.Sleep(time.Second * 1)
- fmt.Println("Signal...")
- cond.Signal() // 1 秒后下發(fā)下一個通知給已經(jīng)獲取鎖的 goroutine
- time.Sleep(time.Second * 1)
- cond.Broadcast() // 1 秒后下發(fā)廣播給所有等待的goroutine
- fmt.Println("Broadcast...")
- time.Sleep(time.Second * 1) // 等待所有 goroutine 執(zhí)行完畢
- }
總結(jié)
在Go中協(xié)程間通信的方式有多種,最常用的是channel。如果牽扯多個協(xié)程的通知,可以使用sync.Cond。
查看channel、sync.Cond源碼之后會發(fā)現(xiàn),它們有相似之處:
- 阻塞協(xié)程統(tǒng)一被封裝在 sudog 結(jié)構(gòu)里面
- channel阻塞讀/寫時,用雙向鏈表存儲被阻塞導(dǎo)致等待喚醒的協(xié)程
- sync.Cond用帶有頭尾指針的單向鏈表存儲被阻塞導(dǎo)致等待喚醒的協(xié)程
- 阻塞時都是使用gopark()進行協(xié)程的掛起操作
雖說有相似之處,但是卻有本質(zhì)區(qū)別:
- channel 可以用來在協(xié)程間傳遞數(shù)據(jù)
- sync.Cond 不可以在協(xié)程間傳遞數(shù)據(jù),主要用來進行協(xié)程的阻塞喚醒操作。如果需要傳遞數(shù)據(jù),則需要全局變量進行傳遞