深入理解 Golang Channel

EllisB62 7年前發布 | 17K 次閱讀 Go語言 Google Go/Golang開發

channel 是 Go 語言中的一個非常重要的特性,這篇文章來深入了解一下 channel。

1. CSP

要想理解 channel 要先知道 CSP 模型。CSP 是 Communicating Sequential Process 的簡稱,中文可以叫做通信順序進程,是一種并發編程模型,由 Tony Hoare 于 1977 年提出。簡單來說,CSP 模型由并發執行的實體(線程或者進程)所組成,實體之間通過發送消息進行通信,這里發送消息時使用的就是通道,或者叫 channel。CSP 模型的關鍵是關注 channel,而不關注發送消息的實體。Go 語言實現了 CSP 部分理論,goroutine 對應 CSP 中并發執行的實體,channel 也就對應著 CSP 中的 channel。

2. channel 基礎知識

2.1 創建 channel

channel 使用之前需要通過 make 創建。

unBufferChan := make(chan int)  // 1
bufferChan := make(chan int, N) // 2

上面的方式 1 創建的是無緩沖 channel,方式 2 創建的是緩沖 channel。如果使用 channel 之前沒有 make,會出現 dead lock 錯誤。至于為什么是 dead lock,下文我們從源碼里面看看。

func main() {
    var x chan int
    go func() {
        x <- 1
    }()
    <-x
}
$ go run channel1.go
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive (nil chan)]:
main.main()
    /Users/kltao/code/go/examples/channl/channel1.go:11 +0x60

goroutine 4 [chan send (nil chan)]:
main.main.func1(0x0)

2.2 channel 讀寫操作

ch := make(chan int, 10)

// 讀操作
x <- ch

// 寫操作
ch <- x

2.3 channel 種類

channel 分為無緩沖 channel 和有緩沖 channel。兩者的區別如下:

  • 無緩沖:發送和接收動作是同時發生的。如果沒有 goroutine 讀取 channel (<- channel),則發送者 (channel <-) 會一直阻塞。

  • 緩沖:緩沖 channel 類似一個有容量的隊列。當隊列滿的時候發送者會阻塞;當隊列空的時候接收者會阻塞。

2.4 關閉 channel

channel 可以通過 built-in 函數 close() 來關閉。

ch := make(chan int)

// 關閉
close(ch)

關于關閉 channel 有幾點需要注意的是:

  • 重復關閉 channel 會導致 panic。
  • 向關閉的 channel 發送數據會 panic。
  • 從關閉的 channel 讀數據不會 panic,但是讀出的數據是 channel 類似的默認值,比如 chan int 類型的 channel 關閉之后讀取到的值為 0。

對于上面的第三點,我們需要區分一下:channel 中的值是默認值還是 channel 關閉了。可以使用 ok-idiom 方式,這種方式在 map 中比較常用。

ch := make(chan int, 10)
...
close(ch)

// ok-idiom 
val, ok := <-ch
if ok == false {
    // channel closed
}

3. channel 的典型用法

1. goroutine 通信

func main() {
    x := make(chan int)
    go func() {
        x <- 1
    }()
    <-x
}

2. select

select 一定程度上可以類比于 linux 中的 IO 多路復用中的 select。后者相當于提供了對多個 IO 事件的統一管理,而 Golang 中的 select 相當于提供了對多個 channel 的統一管理。當然這只是 select 在 channel 上的一種使用方法。

select {
    case e, ok := <-ch1:
        ...
    case e, ok := <-ch2:
        ...
    default:  
}

值得注意的是 select 中的 break 只能跳到 select 這一層。select 使用的時候一般配合 for 循環使用,像下面這樣,因為正常 select 里面的流程也就執行一遍。這么來看 select 中的 break 就稍顯雞肋了。所以使用 break 的時候一般配置 label 使用,label 定義在 for 循環這一層。

for {
    select {
        ...
    }
}

3. range channel

range channel 可以直接取到 channel 中的值。當我們使用 range 來操作 channel 的時候,一旦 channel 關閉,循環自動結束。

func consumer(ch chan int) {
    for x := range ch {
        fmt.Println(x)
        ...
    }
}

func producer(ch chan int) {
  for _, v := range values {
      ch <- v
  }  
}

4. 超時控制

在很多操作情況下都需要超時控制,利用 select 實現超時控制,下面是一個簡單的示例。

select {
  case <- ch:
    // get data from ch
  case <- time.After(2 * time.Second)
    // read data from ch timeout
}

類似的,上面的 time.After 可以換成其他的任何異常控制流。

5. 生產者-消費者模型

利用緩沖 channel 可以很輕松的實現生產者-消費者模型。上面的 range 示例其實就是一個簡單的生產者-消費者模型實現。

4. 單向 channel

單向 channel,顧名思義只能寫或讀的 channel。但是仔細一想,只能寫的 channel,如果不讀其中的值有什么用呢?其實單向 channel 主要用在函數聲明中。比如。

func foo(ch chan<- int) <-chan int {...}

foo 的形參是一個只能寫的 channel,那么就表示函數 foo 只會對 ch 進行寫,當然你傳入的參數可以是個普通 channel。foo 的返回值是一個只能讀的 channel,那么表示 foo 的返回值規范用法就是只能讀取。這種寫法在 Golang 的原生代碼庫中有非常多的示例,感興趣的可以去看一下。

// Done returns a channel which is closed if and when this pipe is closed
// with CloseWithError.
func (p *http2pipe) Done() <-chan struct{} {
    p.mu.Lock()
    defer p.mu.Unlock()
    if p.donec == nil {
        p.donec = make(chan struct{})
        if p.err != nil || p.breakErr != nil {

            p.closeDoneLocked()
        }
    }
    return p.donec
}

也許你會說這么寫在功能上和使用普通的 channel 并不會有什么差別。確實是這樣的。但是使用單向 channel 編程體現了一種非常優秀的編程范式: convention over configuration ,中文一般叫做 約定優于配置 。這種編程范式在 Ruby 中體現的尤為明顯。

5. 總結

Golang 的 channel 將 goroutine 隔離開,并發編程的時候可以將注意力放在 channel 上。在一定程度上,這個和消息隊列的解耦功能還是挺像的。上面主要還是介紹了一些 channel 的常規操作,還有一些奇淫技巧放在參考資料里了。之后的一篇文章還是來看看 channel 的源碼吧,對于更深入地理解 channel 還是挺有用的。

6. 參考

  1. Go Concurrency Patterns: Pipelines and cancellation
  2. Go Concurrency Visualize

 

來自:http://legendtkl.com/2017/07/30/understanding-golang-channel/

 

 本文由用戶 EllisB62 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!