Go中協程間通訊的方式Sync.Cond

大雄45發表於2021-06-15
導讀 在Go中協程間通訊的方式有多種,最常用的是channel。如果牽扯多個協程的通知,可以使用sync.Cond。

Go中協程間通訊的方式Sync.CondGo中協程間通訊的方式Sync.Cond

1. 程式中的通訊方式

GO語言中有句名言:“不要用共享記憶體來通訊,而是使用通訊來共享記憶體”。

程式語言中,通訊方式分為程式間通訊、執行緒間通訊。

1.程式間通訊,常用方式:

  • 有名管道
  • 無名管道
  • 訊號
  • 共享記憶體
  • 訊息佇列
  • 訊號燈集
  • socket

2.執行緒間通訊,常用方式:

  • 訊號量
  • 互斥鎖
  • 條件變數

對於Go語言來說,Go程式啟動之後對外是一個程式,內部包含若干協程,協程相當於使用者態輕量級執行緒,所以協程的通訊方式大多可以使用執行緒間通訊方式來完成。

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

2. 什麼是廣播?

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

程式設計中的廣播可以理解為:多個操作流程依賴於一個操作流程完成後才能進行某種動作,這個被依賴的操作流程在喚醒所有依賴者時使用的一種通知方式。

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

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()  // 阻塞等待,直到被喚醒

對應原始碼追溯

// 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) 
}

使用方法,程式碼示例:

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。

原文來自:

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69955379/viewspace-2776667/,如需轉載,請註明出處,否則將追究法律責任。

相關文章