Go基礎系列:雙層channel用法示例

駿馬金龍發表於2018-11-21

Go channel系列

雙層通道的解釋見Go的雙層通道

以下是一個雙層通道的使用示例。注意下面的示例中使用了”訊號通道”(Signal channel),但這裡的訊號通道是多餘的,僅僅只是為了介紹。

訊號通道不用來傳遞資料,而是用來傳遞訊息,用來產生可讀、可寫的事件,以便讓select選中某個分支。產生訊息事件的方式有多種,比如直接關閉通道、傳送false/true布林值等等

package main

import (
    "fmt"
    "time"
)

func main() {
    // 定義雙層通道cc
    cc := make(chan chan int)
    times := 5
    for i := 1; i < times+1; i++ {
        // 定義訊號通道f
        f := make(chan bool)

        // 每次迴圈都在雙層通道cc中生成內層通道c
        // 並通過訊號通道f來終止f1()
        go f1(cc, f)

        // 從雙層通道cc中取出內層通道ch
        // 並向ch通道傳送資料
        ch := <-cc
        ch <- i

        // 從ch中取出資料
        for sum := range ch {
            fmt.Printf("Sum(%d)=%d
", i, sum)
        }
        // 每個迴圈睡眠一秒鐘
        time.Sleep(time.Second)
        // 每次迴圈都關閉訊號通道f
        close(f)
    }
}

// 雙層通道cc用來生成內層通道c
// 並使用訊號通道f來終止函式f1()
func f1(cc chan chan int, f chan bool) {
    c := make(chan int)
    cc <- c
    defer close(c)
    sum := 0
    select {
    // 從內層通道中取出資料,計算和,然後發回內層通道
    case x := <-c:
        for i := 0; i <= x; i++ {
            sum = sum + i
        }
        // goroutine將阻塞在此,直到資料被讀走
        c <- sum
    // 訊號通道f可讀時,結束f1()的執行
    // 但因為select沒有在for中,該case分支用不上
    case <-f:
        return
    }
}

上面的示例中,函式f1()兩個引數,一個是雙層通道cc,一個是訊號通道f。f1()中首先生成了一個通道c,併傳送給了雙層通道cc,使得main()中可以從cc中取得這個內層通道c,並向其傳送資料。

回到f1()中,select最初會被阻塞,因為內層通道c和訊號通道f都沒有資料可讀。由於main()可以取得內層通道c,並向其傳送資料,使得f1()中的select第一個case分支被選中,該分支會計算髮送的整數之前的總和,並將計算結果重新傳送給內層通道c,讓main()可以取得這個計算結果。

上面的示例中有幾個細節需要注意:

  1. 在f1()中必須關閉內層通道c,因為main()中的range迭代一個未關閉的通道會一直阻塞,而且每次呼叫f1()都會重新建立c通道。
  2. 上面的訊號通道其實沒有起到任何作用。
  3. f1()中的select必須不能放進for迴圈。因為f1()將資料發回c之後,如果在for中,發f()所在的goroutine將阻塞在select上,由於c通道還沒有關閉,這會導致main goroutine因range迭代操作而阻塞,也就是說所有goroutine都被阻塞了,出現了死鎖。

所以,當在select中有傳送操作的時候,很可能會出現死鎖現象。這時,要麼為select加上default,要麼為select加上超時時間,要麼select不要放在for迴圈中

相關文章