Go: 監控模式
Go 能實現監控模式 [1],歸功於 sync
包和 sync.Cond
結構體。監控模式允許 Goroutine 在進入睡眠模式前等待一個定特定條件,而不會阻塞執行或消耗資源。
條件變量
我們舉個例子,來看看這個模式可以帶來的好處。我將使用 Bryan Mills 的演示文稿 [2] 中提供的示例:
type Item = int
type Queue struct {
items []Item
*sync.Cond
}
func NewQueue() *Queue {
q := new(Queue)
q.Cond = sync.NewCond(&sync.Mutex{})
return q
}
func (q *Queue) Put(item Item) {
q.L.Lock()
defer q.L.Unlock()
q.items = append(q.items, item)
q.Signal()
}
func (q *Queue) GetMany(n int) []Item {
q.L.Lock()
defer q.L.Unlock()
for len(q.items) < n {
q.Wait()
}
items := q.items[:n:n]
q.items = q.items[n:]
return items
}
func main() {
q := NewQueue()
var wg sync.WaitGroup
for n := 10; n > 0; n-- {
wg.Add(1)
go func(n int) {
items := q.GetMany(n)
fmt.Printf("%2d: %2d\n", n, items)
wg.Done()
}(n)
}
for i := 0; i < 100; i++ {
q.Put(i)
}
wg.Wait()
}
Queue
是一個非常簡單的結體構,由一個切片和 sync.Cond
結構組成。然後,我們做兩件事:
-
啓動 10 個 goroutines,並將嘗試一次消費 X 個元素。如果這些元素不夠數目,那麼 Goroutine 將進去睡眠狀態並等待被喚醒
-
主 Goroutine 將用 100 個元素填入隊列。每添加一個元素,它將喚醒一個等待消費的 goroutine。
程序的輸出,
4: [31 32 33 34]
8: [10 11 12 13 14 15 16 17]
5: [35 36 37 38 39]
3: [ 7 8 9]
6: [40 41 42 43 44 45]
2: [18 19]
9: [46 47 48 49 50 51 52 53 54]
10: [21 22 23 24 25 26 27 28 29 30]
1: [20]
7: [ 0 1 2 3 4 5 6]
如果多次運行此程序,將獲得不同的輸出。我們可以看到,由於是按批次檢索值的,每個 Goroutine 獲取的值是一個連續的序列。這一點對於理解 sync.Cond
與 channels
的差異很重要。
sync.Cond vs Channels
用單個 channel
解決這個問題並不容易,因爲它會被消費者一個接一個地拉出來。
爲了解決這個問題,Bryan Mills 編寫了一個包含兩個通道組合的等價解決方案(第 65 頁)[3]:
type Item = int
type waiter struct {
n int
c chan []Item
}
type state struct {
items []Item
wait []waiter
}
type Queue struct {
s chan state
}
func NewQueue() *Queue {
s := make(chan state, 1)
s <- state{}
return &Queue{s}
}
func (q *Queue) Put(item Item) {
s := <-q.s
s.items = append(s.items, item)
for len(s.wait) > 0 {
w := s.wait[0]
if len(s.items) < w.n {
break
}
w.c <- s.items[:w.n:w.n]
s.items = s.items[w.n:]
s.wait = s.wait[1:]
}
q.s <- s
}
func (q *Queue) GetMany(n int) []Item {
s := <-q.s
if len(s.wait) == 0 && len(s.items) >= n {
items := s.items[:n:n]
s.items = s.items[n:]
q.s <- s
return items
}
c := make(chan []Item)
s.wait = append(s.wait, waiter{n, c})
q.s <- s
return <-c
}
結果類似:
1: [ 0]
10: [ 1 2 3 4 5 6 7 8 9 10]
5: [11 12 13 14 15]
8: [16 17 18 19 20 21 22 23]
6: [24 25 26 27 28 29]
3: [37 38 39]
7: [30 31 32 33 34 35 36]
9: [46 47 48 49 50 51 52 53 54]
2: [44 45]
4: [40 41 42 43]
在可讀性和語義方面,條件變量在這裏可能有一個小優勢。但是,它也有限制。
注意事項
我們運行包含 100 個元素的基準測試,如示例所示:
WithCond-8 15.7 µ s ± 2%
WithChan-8 19.4 µ s ± 1%
在這裏使用條件變量要快一些。讓我們試試 10k 個元素的基準測試:
WithCond-8 2.84ms ± 1%
WithChan-8 917 µ s ± 1%
可以看到 channel
的速度要快得多。Bryan Mills 在 “飢餓” 部分(第 45 頁)[4] 中解釋了這個問題:
假設我們調用 GetMany(3000) 的同時有一個調用者在密集的循環中執行 GetMany(3)。兩個服務可能幾乎同時醒來,但 GetMany(3) 調用將能夠消耗三個元素,而 GetMany(3000) 將沒有足夠的元素就緒。隊列將保持耗盡狀態,較大的調用將一直阻塞。
該演示文稿還強調了在處理條件變量時我們可能面臨的其他問題。如果模式看起來很簡單,我們在使用它時應該小心。之前看到的例子向我們展示瞭如何更有效地使用 channel
並通過通信進行共享。
內部流程
內部實現非常簡單,基於發號系統。以下是上一個示例的簡單表示:
進入等待模式的每個 Goroutine 將從變量 wait
開始分號,該變量從 0 開始。這表示等待隊列。
然後,每次調用 Signal()
都會增加另一個名爲 notify
的計數器,該計數器代表需要通知或喚醒的 Goroutine 隊列。
我們的 sync.Cond
結構包含一個負責發號的結構:
type notifyList struct {
wait uint32
notify uint32
lock uintptr
head unsafe.Pointer
tail unsafe.Pointer
}
這是就是上面提到的 wait
和 notify
變量。該結構還通過 head
和 tail
保存等待的 Goroutine 的鏈表,其中每個 Goroutine 在其內部結構中保持對所獲取的票號的引用。
當收到信號時,Go 會在鏈表上進行迭代,直到分配給被檢查的 Goroutine 的票號與 notify
變量的編號匹配,如匹配則喚醒當前票號的 goroutine。一旦找到 goroutine,其狀態將從等待模式變爲可運行模式,然後在 Go 調度程序中處理。
如果你想深入瞭解 Go 調度程序,我強烈建議你閱讀 William Kennedy 關於 Go 調度程序的教程 [5]。
https://medium.com/a-journey-with-go/go-monitor-pattern-9decd26fb28
作者:Vincent Blanchon[6] 譯者:咔嘰咔嘰 [7] 校對:DingdingZhou[8]
本文由 GCTT[9] 原創編譯,Go 中文網 [10] 榮譽推出,發佈在 Go 語言中文網公衆號,轉載請聯繫我們授權。
參考資料
[1]
監控模式: https://en.wikipedia.org/wiki/Monitor(synchronization)_
[2]
Bryan Mills 的演示文稿: https://drive.google.com/file/d/1nPdvhB0PutEJzdCq5ms6UI58dp50fcAN/view
[3]
等價解決方案(第 65 頁): https://drive.google.com/file/d/1nPdvhB0PutEJzdCq5ms6UI58dp50fcAN/view
[4]
Bryan Mills 在 “飢餓” 部分(第 45 頁): https://drive.google.com/file/d/1nPdvhB0PutEJzdCq5ms6UI58dp50fcAN/view
[5]
William Kennedy 關於 Go 調度程序的教程: https://www.ardanlabs.com/blog/2018/08/scheduling-in-go-part1.html
[6]
Vincent Blanchon: https://medium.com/@blanchon.vincent
[7]
咔嘰咔嘰: https://github.com/watermelo
[8]
DingdingZhou: https://github.com/DingdingZhou
[9]
GCTT: https://github.com/studygolang/GCTT
[10]
Go 中文網: https://studygolang.com/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/vkLfBYHh-zUpT78AeIsp9Q