Go – Channel 原理

yexiaobai發表於2019-02-16

注:該文原文為 Channel Axioms ,作者是 Dave Cheney,這是他的部落格地址

大部分的新的 Go 程式設計師能快速理解 channel 是作為一個 queue 的值和認同當 channel 是滿的或者是空的時候, 操作是阻塞的概念。

這篇文章探討了 channel 四個不太常見的特性:

  • 給一個 nil channel 傳送資料,造成永遠阻塞
  • 從一個 nil channel 接收資料,造成永遠阻塞
  • 給一個已經關閉的 channel 傳送資料,引起 panic
  • 從一個已經關閉的 channel 接收資料,立即返回一個零值

給一個 nil channel 傳送資料,造成永遠阻塞

這第一個例子對於新來者是有點小驚奇的,它給一個 nil channel 傳送資料,造成永遠阻塞。

以下這個程式將在第5行造成死鎖,因為未初始化的 channel 是 nil 的,其值是

package main

func main() {
        var c chan string
        c <- "let`s get started" // deadlock
}

點選這裡執行

從一個 nil channel 接收資料,造成永遠阻塞

類似的,從一個 nil channel 接收資料,會造成接受者永遠阻塞。

package main

import "fmt"

func main() {
        var c chan string
        fmt.Println(<-c) // deadlock
}

點選這裡執行

為什麼會發生這樣的情況?下面是一個可能的解釋

  • channel 的 buffer 的大小不是型別宣告的一部分,因此它必須是 channel 的值的一部分
  • 如果 channel 未被初始化,它的 buffer 的大小將是0
  • 如果 channel 的 buffer 大小是0,那麼它將沒有 buffer
  • 如果 channel 沒有 buffer,一個傳送將會被阻塞,直到另外一個 goroutine 為接收做好了準備
  • 如果 channel 是 nil 的,並且接收者和傳送者沒有任何互動,他們都會阻塞然後在各自的 channel 中等待以及不再被解除阻塞狀態

給一個已經關閉的 channel 傳送資料,引起 panic

以下程式將有可能 panic,因為在它的兄弟姐妹有時間完成傳送他們的值之前,這第一個 goroutine 在達到10的時候將關閉 channel。

package main

import "fmt"

func main() {
        var c = make(chan int, 100)
        for i := 0; i < 10; i++ {
                go func() {
                        for j := 0; j < 10; j++ {
                                c <- j
                        }
                        close(c)
                }()
        }
        for i := range c {
                fmt.Println(i)
        }
}

點選這裡執行

因此為什麼沒有一個 close() 版本能讓你檢測 channel 是否關閉?

if !isClosed(c) {
        // c isn`t closed, send the value
        c <- v
}

但是這個函式有一個內在的競爭,某個人可能在我們檢查完 isClosed(c) 之後,但是程式碼獲取 c <- v 之前關閉這個 channel。

處理這個問題的方法在被連線在該文章底部的 2nd article 被討論。

從一個已經關閉的 channel 接收資料,立即返回一個零值

這最後一個示例與前一個是相反的,一旦一個 channel 被關閉,它的所有的值都會從 buffer 中流失,channel 將立即返回0值。

package main

import "fmt"

func main() {
            c := make(chan int, 3)
            c <- 1
            c <- 2
            c <- 3
            close(c)
            for i := 0; i < 4; i++ {
                        fmt.Printf("%d ", <-c) // prints 1 2 3 0
            }
}

點選這裡執行

針對這個問題的正確的解決辦法是使用 range 迴圈處理:

for v := range c {
            // do something with v
}

for v, ok := <- c; ok ; v, ok = <- c {
            // do something with v
}

這兩個語句在函式中是相等的,展示 range 是做什麼。

擴充套件閱讀

相關文章