Golang Channel
channels 是一種類型安全的消息隊列, 充當兩個 goroutine 之間的管道, 將通過它同步的進行任意資源的交換. chan 控制 goroutines 交互的能力從而創建了 Go 同步機制. 當創建的 chan 沒有容量時, 稱為無緩衝channel.反過來, 使用容量創建的 chan 稱為緩衝channel. 要了解通過 chan 交互的 goroutine 的同步行為是什麼, 我們需要知道channel的類型和狀態. 根據我們使用的是無緩衝channel還是緩衝channel, 場景會有所不同, 所以讓我們單獨討論每個場景.
Unbuffered Channels
ch := make(chan struct{})
無緩衝 chan 沒有容量, 因此進行任何交換前需要兩個 goroutine 同時準備好.當 goroutine 試圖將一個資源發送到一個無緩衝的通道並且沒有goroutine 等待接收該資源時, 該通道將鎖住發送 goroutine 並使其等待.當 goroutine 嘗試從無緩衝通道接收, 並且沒有 goroutine 等待發送資源時, 該通道將鎖住接收 goroutine 並使其等待.
無緩衝信道的本質是保證同步.

第一個 goroutine 在發送消息 foo 之後被阻塞, 因為還沒有接收者準備好. 規範中對這種行為進行了很好的解釋:https://golang.org/ref/spec#Channel_types
"If the capacity is zero or absent, the channel is unbuffered and communication succeeds only when both a sender and receiver are ready."
https://golang.org/doc/effective_go.html#channels "If the channel is unbuffered, the sender blocks until the receiver has received the value"
- Receive 先於 Send 發生.
- 好處: 100% 保證能收到.
- 代價: 延遲時間未知.
package main
import (
"fmt"
"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)
fmt.Println(`Message:` + <-c)
}()
wg.Wait()
}
/*
Message:foo
*/
Buffered Channels
buffered channel 具有容量, 因此其行為可能有點不同. 當 goroutine 試圖將資源發送到緩衝通道, 而該通道已滿時, 該通道將鎖住 goroutine並使其等待緩衝區可用. 如果通道中有空間, 發送可以立即進行, goroutine 可以繼續. 當goroutine 試圖從緩衝通道接收數據, 而緩衝通道為空時, 該通道將鎖住 goroutine 並使其等待資源被發送.

package main
import (
"fmt"
"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)
fmt.Println(`Message:` + <-c)
fmt.Println(`Message:` + <-c)
}()
wg.Wait()
}
Latencies due to under-sized buffer
我們在 chan 創建過程中定義的緩衝區大小可能會極大地影響性能。
我將使用密集使用 chan 的扇出模式來查看不同緩衝區大小的影響。
在我們的基準測試中,一個 producer 將在通道中註入百萬個整數元素,
而5個 worker 將讀取並將它們追加到一個名為 total 的結果變量中。

- Send 先於 Receive 發生。
- 好處: 延遲更小。
- 代價: 不保證數據到達,越大的 buffer,越小的保障到達。 buffer = 1 時,給你延遲一個消息的保障。
- 因為 unbuffer channel 的 goroutine 會不斷喚醒睡眠, context switch. 所以比 buffer channel 還慢.
Overlord是嗶哩嗶哩基於Go語言編寫的memcache和redis&cluster的代理及集群管理功能,致力於提供自動化高可用的緩存服務解決方案 https://github.com/bilibili/overlord
Go Concurrency Patterns
- Timing out
- Moving on
- Pipeline
- Fan-out, Fan-in
- Cancellation
- Close 先於 Receive 發生(類似 Buffered)。
- 不需要傳遞數據,或者傳遞 nil。
- 非常適合去做超時控制。
- Context
https://go.dev/blog/concurrency-timeouts https://blog.golang.org/pipelines https://talks.golang.org/2013/advconc.slide#1 https://github.com/go-kratos/kratos/tree/master/pkg/sync
Design Philosophy
- If any given Send on a channel CAN cause the sending goroutine to block:
- Not allowed to use a Buffered channel larger than 1.
- Buffers larger than 1 must have reason/measurements.
- Must know what happens when the sending goroutine blocks.
- Not allowed to use a Buffered channel larger than 1.
- If any given Send on a channel WON’T cause the sending goroutine to block:
- You have the exact number of buffers for each send.
- Fan Out pattern
- You have the buffer measured for max capacity.
- Drop pattern
- You have the exact number of buffers for each send.
- Less is more with buffers.
- (不要把buffer size當作效能的提升, 他只是 latencies變小, 能緩衝的數據變多了, 吞吐要靠多個goroutine來消費)
- Don’t think about performance when thinking about buffers.
- Buffers can help to reduce blocking latency between signaling.
- Reducing blocking latency towards zero does not necessarily mean better throughput.
- If a buffer of one is giving you good enough throughput then keep it.
- Question buffers that are larger than one and measure for size.
- Find the smallest buffer possible that provides good enough throughput.
The Channel Closing Prirciple
- 在沒有修改channel的狀態時, 此時沒有一個簡單通用的方式可以去檢查channel 是否關閉
- 關閉一個 close的 channel 會發生 panic.
將值發送到一個關閉的 channel 會發生 panic
不要在接收端關閉 channel
- 如果有多個發送端, 也不要在發送端關閉 channel
- 只有一個發送端, 才在發送端關閉 channel
- 不要關閉已關閉的 channel
- 不要送值到已關閉的 channel
- 有方向的 channel 不可被關閉
| No | Situation | dataCh | stopCh | toStop | closing | closed | middle layer |
|---|---|---|---|---|---|---|---|
| 01 | M Receivers 1 Sender | 從 Sender 關閉 | x | x | x | x | x |
| 02 | 1 Receiver N Senders | 不必關閉 | Receiver: Close(stopCh) | x | x | x | x |
| 03 | M Receivers N Senders | 不必關閉 | 引用 moderator: 角色來關閉額外的訊號通到 close(stopCh) | 能讓任何 receiver 和 sender 閉數據通道 -> 發送訊號到 toStop channel select {case toStop <- "sender#" + id:default:} |
x | x | x |
| 04 | M Receivers 1 Sender Closed By 3Party | Sender 收到 stop() 發出的 closing 信號後, close(dataCh) |
x | x | 第三方 goroutine call stop() function stop := func() {select {case closing <- struct{}{}:<-closedcase <-closed:}} |
Sender 收到 stop() 發出的 closing 信號 -> return -> run defer function close(closed);close(dataCh); |
x |
| 05 | M Receivers N Senders Closed By 3Party | 在 middle layer close(dataCh) | x | x | third party 呼叫 stop function: 發出 closing <- by | 在 middle layer close(closed) | 需要藉由 middle layer 將 N 個 Sender, 轉換成 1 個 sender 讓此情境變成類似成 4.1: M receivers and 1 Sender, 由sender close dataCh channel |