Go中多協程協做之sync.Cond

1. 程序中的通訊方式

GO語言中有句名言:「不要用共享內存來通訊,而是使用通訊來共享內存」。算法

編程語言中,通訊方式分爲進程間通訊、線程間通訊。編程

  1. 進程間通訊,經常使用方式:
  • 有名管道
  • 無名管道
  • 信號
  • 共享內存
  • 消息隊列
  • 信號燈集
  • socket
  1. 線程間通訊,經常使用方式:
  • 信號量
  • 互斥鎖
  • 條件變量

對於Go語言來講,Go程序啓動以後對外是一個進程,內部包含若干協程,協程至關於用戶態輕量級線程,因此協程的通訊方式大多可使用線程間通訊方式來完成。架構

協程間通訊方式,官方推薦使用channel,channel在一對一的協程之間進行數據交換與通訊十分便捷。可是,一對多的廣播場景中,則顯得有點無力,此時就須要sync.Cond來輔助。併發

2. 什麼是廣播?

舉個例子,上高中時,宿管老師天天早晨須要叫醒學生們去上課。有兩種方法:①一個寢室一個寢室把學生叫醒 ②在宿舍樓安裝廣播,到起牀時間,在廣播上叫醒學生。顯然,使用廣播的方式效率更高。

編程中的廣播能夠理解爲:多個操做流程依賴於一個操做流程完成後才能進行某種動做,這個被依賴的操做流程在喚醒全部依賴者時使用的一種通知方式。less

在Go語言中,則可使用sync.Cond來實現多個協程之間的廣播通知功能。socket

3. sync.Cond

cond是sync包下面的一種數據類型,至關於線程間通訊的條件變量方式。編程語言

`// 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  // 在第一次使用後不可複製,使用go vet做爲檢測使用`
 `// L is held while observing or changing the condition`
 `// 根據需求初始化不一樣的鎖,如*Mutex 和 *RWMutex。注意是 指針類型`
 `L Locker`
 `// 具備頭尾指針的鏈表。存儲被阻塞的協程,通知時操做該鏈表中的協程`
 `notify  notifyList`
 `checker copyChecker  // 複製檢查,檢查cond實例是否被複制`
`}`

該數據類型提供的方法有:分佈式

`type Cond`
`func NewCond(l Locker) *Cond`
`func (c *Cond) Broadcast() // 通知全部協程,廣播`
`func (c *Cond) Signal()  // 通知一個協程`
`func (c *Cond) Wait()  // 阻塞等待,直到被喚醒`

對應源碼追溯oop

`// 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是不是被複制的,若是是就panic`
 `c.checker.check()`
 `// 獲取等待隊列的一個ticket數值,做爲喚醒時的一個令牌憑證`
 `t := runtime_notifyListAdd(&c.notify)`
 `// 解鎖`
 `c.L.Unlock()`
 
 `// 注意,上面的ticket數值會做爲阻塞攜程的一個標識`
 `// 加入通知隊列裏面`
 `// 到這裏執行gopark(),當前協程掛起,直到signal或broadcast發起通知`
 `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()`
 `// 通知全部阻塞等待的協程`
 `// 主要是喚醒 cond.notify 鏈表上的各個協程`
 `runtime_notifyListNotifyAll(&c.notify)`
`}`

使用方法,代碼示例:ui

`var locker sync.Mutex`
`var cond = sync.NewCond(&locker)`
`// NewCond(l Locker)裏面定義的是一個接口,擁有lock和unlock方法。`
`// 看到sync.Mutex的方法,func (m *Mutex) Lock(),能夠看到是指針有這兩個方法,因此應該傳遞的是指針`
`func main() {`
 `// 啓動多個協程`
 `for i := 0; i < 10; i++ {`
 `gofunc(x int) {`
 `cond.L.Lock()          // 獲取鎖`
 `defer cond.L.Unlock()  // 釋放鎖`
 
 `cond.Wait()   // 等待通知,阻塞當前 goroutine`
 
 `// 通知到來的時候, cond.Wait()就會結束阻塞, do something. 這裏僅打印`
 `fmt.Println(x)`
 `}(i)`
 `}`
 
 `time.Sleep(time.Second * 1) // 睡眠 1 秒,等待全部 goroutine 進入 Wait 阻塞狀態`
 `fmt.Println("Signal...")`
 `cond.Signal()               // 1 秒後下發一個通知給已經獲取鎖的 goroutine`
 
 `time.Sleep(time.Second * 1)`
 `fmt.Println("Signal...")`
 `cond.Signal()               // 1 秒後下發下一個通知給已經獲取鎖的 goroutine`
 
 `time.Sleep(time.Second * 1)`
 `cond.Broadcast()            // 1 秒後下發廣播給全部等待的goroutine`
 `fmt.Println("Broadcast...")`
 `time.Sleep(time.Second * 1) // 等待全部 goroutine 執行完畢`
`}`

總結

在go中協程間通訊的方式有多種,最經常使用的是channel。若是牽扯多個協程的通知,可使用sync.Cond。

查看channel、sync.Cond源碼會發現,它們有類似之處

  1. 阻塞協程統一被封裝在 sudog 結構裏面
  2. channel 阻塞讀/寫時,用雙向鏈表存儲被阻塞致使等待喚醒的協程
  3. sync.Cond 使用帶有頭尾指針的單向鏈表存儲被阻塞致使等待喚醒的協程
  4. 阻塞時都是使用gopark()進行協程的掛起操做

雖然說有類似之處,但卻有本質區別

  1. channel 可用來在協程間傳遞數據
  2. sync.Cond 不可用來在協程間傳遞數據,主要用來進行協程的阻塞喚醒操做。如須要傳遞數據,則須要使用全局變量進行傳遞

- THE END -

推薦閱讀  

我眼中的C語言及其起源

初識Golang彙編

關於中文編程的一些思考

併發編程中爲何須要加鎖

個人魔幻算法刷題之路

如何構建一份互聯網架構圖

帶你認識分佈式系統的CAP理論

關注加星標,瞭解「編程技術之道」

相關文章
相關標籤/搜索