這一次,徹底搞懂 Go Cond


hi,大家好,我是 haohongfan。

本篇文章會從源碼角度去深入剖析下 sync.Cond。Go 日常開發中 sync.Cond 可能是我們用的較少的控制並發的手段,因為大部分場景下都被 Channel 代替了。還有就是 sync.Cond 使用確實也蠻復雜的。

比如下面這段代碼:

package main

import (
	"fmt"
	"time"
)

func main() {
	done := make(chan int, 1)

	go func() {
		time.Sleep(5 * time.Second)
		done <- 1
	}()

	fmt.Println("waiting")
	<-done
	fmt.Println("done")
}

同樣可以使用 sync.Cond 來實現

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	cond := sync.NewCond(&sync.Mutex{})
	var flag bool
	go func() {
		time.Sleep(time.Second * 5)
		cond.L.Lock()
		flag = true
		cond.Signal()
		cond.L.Unlock()
	}()

	fmt.Println("waiting")
	cond.L.Lock()
	for !flag {
		cond.Wait()
	}
	cond.L.Unlock()
	fmt.Println("done")
}

大部分場景下使用 channel 是比 sync.Cond方便的。不過我們要注意到,sync.Cond 提供了 Broadcast 方法,可以通知所有的等待者。想利用 channel 實現這個方法還是不容易的。我想這應該是 sync.Cond 唯一有用武之地的地方。

先列出來一些問題吧,可以帶着這些問題來閱讀本文:

  1. cond.Wait本身就是阻塞狀態,為什么 cond.Wait 需要在循環內 ?
  2. sync.Cond 如何觸發不能復制的 panic ?
  3. 為什么 sync.Cond 不能被復制 ?
  4. cond.Signal 是如何通知一個等待的 goroutine ?
  5. cond.Broadcast 是如何通知等待的 goroutine 的?

源碼剖析

cond wait

cond signal

cond broadcast

cond 排隊

cond.Wait 是阻塞的嗎?是如何阻塞的?

是阻塞的。不過不是 sleep 這樣阻塞的。

調用 goparkunlock 解除當前 goroutine 的 m 的綁定關系,將當前 goroutine 狀態機切換為等待狀態。等待后續 goready 函數時候能夠恢復現場。

cond.Signal 是如何通知一個等待的 goroutine ?

  1. 判斷是否有沒有被喚醒的 goroutine,如果都已經喚醒了,直接就返回了
  2. 將已通知 goroutine 的數量加1
  3. 從等待喚醒的 goroutine 隊列中,獲取 head 指針指向的 goroutine,將其重新加入調度
  4. 被阻塞的 goroutine 可以繼續執行

cond.Broadcast 是如何通知等待的 goroutine 的?

  1. 判斷是否有沒有被喚醒的 goroutine,如果都已經喚醒了,直接就返回了
  2. 將等待通知的 goroutine 數量和已經通知過的 goroutine 數量設置成相等
  3. 遍歷等待喚醒的 goroutine 隊列,將所有的等待的 goroutine 都重新加入調度
  4. 所有被阻塞的 goroutine 可以繼續執行

cond.Wait本身就是阻塞狀態,為什么 cond.Wait 需要在循環內 ?

我們能注意到,調用 cond.Wait 的位置,使用的是 for 的方式來調用 wait 函數,而不是使用 if 語句。

這是由於 wait 函數被喚醒時,存在虛假喚醒等情況,導致喚醒后發現,條件依舊不成立。因此需要使用 for 語句來循環地進行等待,直到條件成立為止。

使用中注意點

1. 不能不加鎖直接調用 cond.Wait

func (c *Cond) Wait() {
	c.checker.check()
	t := runtime_notifyListAdd(&c.notify)
	c.L.Unlock()
	runtime_notifyListWait(&c.notify, t)
	c.L.Lock()
}

我們看到 Wait 內部會先調用 c.L.Unlock(),來先釋放鎖。如果調用方不先加鎖的話,會觸發“fatal error: sync: unlock of unlocked mutex”。關於 mutex 的使用方法,推薦閱讀下《這可能是最容易理解的 Go Mutex 源碼剖析》

2. 為什么不能 sync.Cond 不能復制 ?

sync.Cond 不能被復制的原因,並不是因為 sync.Cond 內部嵌套了 Locker。因為 NewCond 時傳入的 Mutex/RWMutex 指針,對於 Mutex 指針復制是沒有問題的。

主要原因是 sync.Cond 內部是維護着一個 notifyList。如果這個隊列被復制的話,那么就在並發場景下導致不同 goroutine 之間操作的 notifyList.wait、notifyList.notify 並不是同一個,這會導致出現有些 goroutine 會一直堵塞。

這里有留下一個問題,sync.Cond 內部是有一段代碼 check sync.Cond 是不能被復制的,下面這段代碼能觸發這個 panic 嗎?

package main

import (
	"fmt"
	"sync"
)

func main() {
	cond1 := sync.NewCond(new(sync.Mutex))
	cond := *cond1
	fmt.Println(cond)
}

有興趣的可以動手嘗試下,以及嘗試下如何才能觸發這個panic "sync.Cond is copied” 。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM