- 本文主要講實踐,原理部分會一筆帶過,關於 go 語言並發實現和記憶體模型後續會有文章。
- channel 實現的源碼不複雜,推薦閱讀,
https://github.com/golang/go/blob/master/src/runtime/chan.go
channel 是幹什麼的
意義:channel 是用來通訊的
實際上:(資料拷貝了一份,並通過 channel 傳遞,本質就是個隊列)
channel 應該用在什麼地方
核心:需要通訊的地方
例如以下情境:
記住!channel 不是用來實現鎖機制的,雖然有些地方可以用它來實作類別似讀寫鎖,保護臨界區的功能,但不要這麼用!
channel 用例實現
逾時控制
// 利用 time.After 實現func main() { done := do() select { case <-done: // logic case <-time.After(3 * time.Second): // timeout }}func do() <-chan struct{} { done := make(chan struct{}) go func() { // do something // ... done <- struct{}{} }() return done}
取最快的結果
比較常見的一個情境是重試,第一個請求在指定逾時時間內沒有返回結果,這時重試第二次,取兩次中最快返回的結果使用。
逾時控制在上面有,下面代碼部分就簡單實現調用多次了。
func main() { ret := make(chan string, 3) for i := 0; i < cap(ret); i++ { go call(ret) } fmt.Println(<-ret)}func call(ret chan<- string) { // do something // ... ret <- "result"}
限制最大並發數
// 最大並發數為 2limits := make(chan struct{}, 2)for i := 0; i < 10; i++ { go func() { // 緩衝區滿了就會阻塞在這 limits <- struct{}{} do() <-limits }()}
for...range 優先
for ... range c { do }
這種寫法相當於 if _, ok := <-c; ok { do }
func main() { c := make(chan int, 20) go func() { for i := 0; i < 10; i++ { c <- i } close(c) }() // 當 c 被關閉後,取完裡面的元素就會跳出迴圈 for x := range c { fmt.Println(x) }}
多個 goroutine 同步響應
利用 close 廣播
func main() { c := make(chan struct{}) for i := 0; i < 5; i++ { go do(c) } close(c)}func do(c <-chan struct{}) { // 會阻塞直到收到 close <-c fmt.Println("hello")}
非阻塞的 select
select 本身是阻塞的,當所有分支都不滿足就會一直阻塞,如果想不阻塞,那麼一個什麼都不乾的 default 分支是最好的選擇
select {case <-done: returndefault: }
for{select{}} 終止
盡量不要用 break label 形式,而是把終止迴圈的條件放到 for 條件裡來實現
for ok { select { case ch <- 0: case <-done: ok = false }}
未完待續
...
channel 特性
基礎特性
操作 |
值為 nil 的 channel |
被關閉的 channel |
正常的 channel |
close |
panic |
panic |
成功關閉 |
c<- |
永遠阻塞 |
panic |
阻塞或成功發送 |
<-c |
永遠阻塞 |
永遠不阻塞 |
阻塞或成功接收 |
happens-before 特性
- 無緩衝時,接收 happens-before 發送
- 任何情況下,發送 happens-before 接收
- close happens-before 接收
參考
- https://go101.org/article/channel.html
- https://golang.org/doc/effective_go.html#channels