Go:無緩衝和有緩衝通道
"Go 之旅 插圖,由 Go Gopher 的 Renee French 創作
Go 中的通道(channel)機制十分強大,但是理解內在的概念甚至可以使它更強大。實際上,選擇緩衝通道或無緩衝通道將改變應用程序的行爲和性能。
本文是 Go 語言中文網組織的 GCTT 翻譯,發佈在 Go 語言中文網公衆號,轉載請聯繫我們授權。
無緩衝通道
無緩衝通道是在消息發送到通道時需要接收器的通道。聲明一個無緩衝通道時,你不需要聲明容量。例如:
package main
import (
"sync"
"time"
)
func main() {
c := make(chan string)
var wg sync.WaitGroup
wg.Add(2)
Go func() {
defer wg.Done()
c <- `foo`
}()
Go func() {
defer wg.Done()
time.Sleep(time.Second * 1)
println(`Message: `+ <-c)
}()
wg.Wait()
}
由於沒有準備就緒的接收者,第一個 goroutine
在發送消息 foo
時將被阻塞。這個說明文檔 [1] 很好地解釋了這種行爲:
如果容量爲零或未設置,則通道將被無緩衝,只有在發送方和接收方都準備就緒時通信才能成功。
這一點,《Effective Go》[2] 中描述的也很清晰:
如果通道是無緩衝的,發送者將被阻塞,直到接收者接收到值。
通道的內部描繪可以給我們更多關於此行爲的有趣的細節
無緩衝通道內部結構
channel
結構體 在 runtime
包的 chan.go
文件中可以找到。該結構包含與通道緩衝區相關的屬性,但是爲了說明無緩存的通道,我將省略我們稍後將看到的那些屬性。下面是無緩衝通道的示意圖:
hchan 結構
通道維護了指向接收方( recvq
)和發送方( sendq
)列表的指針,由鏈表 waitq.sudog
表示 ,包含指向下一個元素的指針(next)和指向上一個元素的指針(previous),以及與處理 接收方 / 發送方 的 Goroutine 相關的信息。有了這些信息,Go 程序就很容易知道,如果沒有了發送方,通道就應該阻塞接收方,反之,沒有了接收方,通道就應該阻塞發送方。
下面是我們前面示例的工作流:
-
通道是用一個空的接收方和發送方列表創建的。
-
第 16 行,我們的第一個 Goroutine 將值
foo
發送到通道。 -
通道從(緩衝)池中獲取一個結構體
sudog
,用以表示發送者。這個結構將維護對 Goroutine 和值foo
的引用。 -
這個發送者現在進入隊列(enqueued )
sendq
。 -
由於 “chan send” 阻塞,goroutine 進入等待狀態。
-
第 23 行,我們的第二個 Goroutine 將讀取來自通道的消息。
-
通道將彈出
sendq
隊列,以獲取步驟 3 中的等待發送的結構體。 -
通道將使用
memmove
函數將發送方發送的值 (封裝裝在sudog
結構中) 複製到讀取的通道的變量。 -
現在,我們的第一個 Goroutine 可以恢復在第 5 步,並將釋放在第 3 步獲得的
sudog
。
正如我們在工作流中再次看到的,goroutine 必須切換到等待,直到接收器可用爲止。但是,如果需要,這種阻塞行爲可以通過緩衝通道避免。
緩衝通道內部結構
稍微改動之前的例子,以添加一個緩衝區:
package main
import (
"sync"
"time"
)
func main() {
c := make(chan string, 2)
var wg sync.WaitGroup
wg.Add(2)
Go func() {
defer wg.Done()
c <- `foo`
c <- `bar`
}()
Go func() {
defer wg.Done()
time.Sleep(time.Second * 1)
println(`Message: `+ <-c)
println(`Message: `+ <-c)
}()
wg.Wait()
}
現在讓我們根據這個例子分析結構 hchan
和與緩衝區相關的字段:
緩衝通道的 hchan 結構
buffer(緩衝)由以下五個屬性組成:
-
qcount
存儲緩衝區中元素的當前數量 -
dataqsiz
存儲緩衝區中最大元素的數量 -
buf
指向一個內存段,該內存段包含緩衝區中元素的最大數量的空間 -
sendx
存儲緩衝區中的位置,以便通道接收下一個元素 -
recvx
在緩衝區中存儲通道返回的下一個元素的位置
通過 sendx
和 recvx
,這個緩衝區就像一個循環隊列:
通道結構中的循環隊列
這個循環隊列允許我們在緩衝區中維護一個順序,而不需要在其中一個元素從緩衝區彈出時不斷移動元素。
正如我們在前一節中看到的那樣,一旦達到緩衝區的上限,嘗試在緩衝區中發送元素的 Goroutine 將被移動到發送者列表中,並切換到等待狀態。然後,一旦程序讀取緩衝區,從緩衝區中返回位於 recvx
位置的元素,將釋放等待的 Goroutine ,它的值將被推入緩衝中。這種屬性使 通道有 FIFO(先進先出)[3] 的行爲。
由於緩衝區大小不足造成的延遲
我們在通道創建期間定義的緩衝區大小可能會極大地影響性能。我使用扇出模式來密集使用通道,以查看不同緩衝區大小的影響。以下是一些壓力測試:
package bench
import (
"sync"
"sync/atomic"
"testing"
)
func BenchmarkWithNoBuffer(b *testing.B) {
benchmarkWithBuffer(b, 0)
}
func BenchmarkWithBufferSizeOf1(b *testing.B) {
benchmarkWithBuffer(b, 1)
}
func BenchmarkWithBufferSizeEqualsToNumberOfWorker(b *testing.B) {
benchmarkWithBuffer(b, 5)
}
func BenchmarkWithBufferSizeExceedsNumberOfWorker(b *testing.B) {
benchmarkWithBuffer(b, 25)
}
func benchmarkWithBuffer(b *testing.B, size int) {
for i := 0; i < b.N; i++ {
c := make(chan uint32, size)
var wg sync.WaitGroup
wg.Add(1)
Go func() {
defer wg.Done()
for i := uint32(0); i < 1000; i++ {
c <- i%2
}
close(c)
}()
var total uint32
for w := 0; w < 5; w++ {
wg.Add(1)
Go func() {
defer wg.Done()
for {
v, ok := <-c
if !ok {
break
}
atomic.AddUint32(&total, v)
}
}()
}
wg.Wait()
}
}
在我們的基準測試中,一個生產者將在通道中注入一個 100 萬個整數元素,而十個消費者將讀取他們,並將它們累加到一個名爲 total
的結果變量中。
我使用 benchstat
運行他們 10 次來分析結果:
name time/op
WithNoBuffer-8 306 µ s ± 3%
WithBufferSizeOf1-8 248 µ s ± 1%
WithBufferSizeEqualsToNumberOfWorker-8 183 µ s ± 4%
WithBufferSizeExceedsNumberOfWorker-8 134 µ s ± 2%
一個適當大小的緩衝區確實可以使您的應用程序更快!讓我們跟蹤分析基準測試,以確定延遲在哪裏。
追蹤延遲
跟蹤基準測試將使您訪問同步阻塞概要文件,該概要文件顯示等待同步原語的 goroutines 阻塞位於何處。Goroutines 在同步過程中花費了 9ms 的時間來等待無緩衝通道的值,而 50 大小的緩衝區只等待 1.9ms:
同步阻塞概要
由於緩衝的存在,來自發送值的等待延遲減小了 5 倍:
同步阻塞概要
我們現在確實證實了我們以前的懷疑。緩衝區的大小對應用程序的性能有重要影響。
via: https://medium.com/a-journey-with-go/go-buffered-and-unbuffered-channels-29a107c00268
作者:Vincent Blanchon[4] 譯者:TomatoAres[5] 校對:DingdingZhou[6]
本文由 GCTT[7] 原創編譯,Go 中文網 [8] 榮譽推出,發佈在 Go 語言中文網公衆號,轉載請聯繫我們授權。
參考資料
[1]
說明文檔: https://golang.org/ref/spec#Channel_types
[2]
《Effective Go》: https://golang.org/doc/effective_go.html#channels
[3]
FIFO(先進先出): http://lsm6ds3%20fifo%20pattern/
[4]
Vincent Blanchon: https://medium.com/@blanchon.vincent
[5]
TomatoAres: https://github.com/TomatoAres
[6]
DingdingZhou: https://github.com/DingdingZhou
[7]
GCTT: https://github.com/studygolang/GCTT
[8]
Go 中文網: https://studygolang.com/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/WvlVQuyMsPQzWJqBW6qQ_g