Go36-10,11-通道

通道

通道(channel)是Go語言的併發編程模式中重要的一員。通道類型的值本身就是併發安全的,這也是Go語言自帶的、唯一一個可以滿足併發安全性的類型。

使用通道

聲明一個通道類型變量,需要確定該通道類型的元素類型,這決定了可以通過這個通道傳遞什麼類型的數據。
初始化通道,需要用到Go語言的內建函數make。make函數除了必須接收類型字面量作爲參數,還可以接收一個int類型的參數。第二個參數是可選的,表示通道的容量。
通道的容量,指通道最多可以緩存多少個元素:

  • 當容量爲0時,未設置第二個參數也是0,通道爲非緩衝通道
  • 當容量大於0時,通過第二個參數指定了容量,通道爲緩衝通道

一個通道相當於一個先進先出(FIFO)的隊列。
通道中的各個元素值都是嚴格地按照發送的順序排列的,先被髮送通道的元素值一定會先被接收。
元素值的發送和接收都需要用到操作符<-。可以叫它接送操作符。一個左尖括號緊接着一個減號形象地代表了元素值的傳輸方向。

package main

import "fmt"

func main() {
    ch1 := make(chan int, 3)
    ch1 <- 1
    ch1 <- 2
    ch1 <- 3
    tmp := <- ch1
    fmt.Println(tmp)
    fmt.Println(<- ch1)
    fmt.Println(<- ch1)
}

通道的特性

通道的基本特性:

  1. 發送操作之間是互斥的,接收操作之間也是互斥的
  2. 發送操作和接收操作中對元素值的處理都是不可分割的
  3. 發送操作在完全完成之前會被阻塞,接收操作也是這樣

特性一
在同一時刻,在運行時,系統只會執行對同一個通道的任意個發送操作中的某一個。知道這個元素值被完全的賦值進通道後,其他針對該通道的發送操作纔可能被執行。接收操作也是這樣。

元素值的複製
元素值從外界進入通道時會被複制。就是說進入通道的並不是那個元素值,而是它的副本。
元素值從通道進入外界是會被移動。這個移動包含2步,先是生成元素值的副本給接收方,然後刪除在通道中的這個元素值。

特性二
不可分割的意思,如果是發送操作,就是要複製元素值,一旦開始執行,一定會複製完畢。 不會出現值賦值了一部分的情況。
如果是接收操作,這裏有2步,在生成副本後一定會刪除掉通道中的元素值。不會出現通道中有殘留原來的副本的情況。
這是爲了保證通道中元素值的完整性,也是爲了保證通道操作的唯一性。

特性三
發送操作包括了“複製元素值”和“放置副本到通道內部”這兩個步驟。在這兩個步驟完成之前,那句代碼會一直阻塞在那裏。在通道完成發送操作之後,系統會通知這句代碼所在的goroutine,使它可以去爭取繼續運行代碼的機會。
接收操作包括了“複製通道內的元素值”、“放置副本到接收方”和“刪掉原值”這三個步驟。完成全部操作執行,同樣也是阻塞的。
如此阻塞代碼,其實就是爲了實現操作的互斥(特性一)和元素值的完整(特性二)。

阻塞的問題

這裏分別講 非緩衝通道 和 緩衝通道 的情況。

緩衝通道
如果通道已滿,那麼對它的所有發送操作都會被阻塞,直到通道中有元素值被接收走。
對於有多個阻塞的發送操作,會優先通知最早的那個因爲通道滿了而等待的、那個發送操作所在的goroutinr,於是會在收到通知後再次執行發送操作。由於發送操作在阻塞後,所在的goroutine會順序進入通道內部的發送等待隊列,所以通知的順序總是公平的。
如果通道已空,那麼對它的所有接受操作都會被阻塞,知道通道中有新的元素值出現。這裏通道內部也有個接受等待隊列,保證通知執行接收操作的順序。

非緩衝通道
情況簡單一些,無論是發送操作還是接收操作,一開始執行就會被阻塞,直到配對的操作也開始執行。

非緩衝通道是在用同步的方式傳遞數據。就是隻有收發雙方對接上,數據纔會被傳送。並且數據是直接從發送方賦值到接收方的,中間不會用非緩衝通道做中轉。
緩衝通道則是在用異步的方式傳遞數據。緩衝通道會作爲收發雙方的中間件,元素值是先從發送方賦值的緩衝通道,之後再由緩衝通道賦值給接收方。但是,當發送操作在執行的時候發泄空的通道中,正好有等待的接收操作是,會直接把元素複製給接收方。

值爲nil的通道
就是未做初始化的通道,對它的發送或接收操作都會永久的處於阻塞狀態。這是個錯誤使用通道而造成阻塞的情況。所以不要忘記初始化通道。

關閉通道

上面說了不要忘記初始化通道。對於已經初始化的通道,收發操作一定不會引發Panic。除非通道關閉,通道一旦關閉,再對它接你發送操作,就會引發Panic。
關閉通道的操作只能執行一次,嘗試關閉一個已經關閉了的通道,也會引發Panic。
接收操作,關閉通道後,不會影響接收通道內還未取出的值,也就是說是可以繼續取值的。即使值取完了,接收操作也還能一直取到值,不會阻塞,此時取到的是通道內類型的零值:

package main

import "fmt"

func main() {
    ch1 := make(chan int, 3)
    ch1 <- 1
    ch1 <- 2
    ch1 <- 3
    fmt.Println(<- ch1)
    close(ch1)  // 關閉通道不影響取值
    // ch1 <- 4  // 關閉後不能再發送值了
    fmt.Println(<- ch1)
    fmt.Println(<- ch1)
    fmt.Println(<- ch1)  // 取完了,就一直返回零值
    fmt.Println(<- ch1)
    fmt.Println(<- ch1)
}

接收操作,是可以感知到通道關閉的,並能夠安全退出。接收表達式可以返回兩個變量。第二個變量是bool類型,一般用ok命名。如果返回值爲false就說明通道已經關閉了,並且再沒有元素值可取了。注意是並且,也就是如果通道關閉,但是裏面還有未取出的元素,返回的還是true。可以參考一下如下示例的效果:

package main

import "fmt"

func main() {
    ch1 := make(chan int, 2)
    // 發送方
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println("發送方發送值:", i)
            ch1 <- i
        }
        fmt.Println("發送完畢,關閉通道。")
        close(ch1)
    }()
    // 接收方
    for {
        elem, ok := <- ch1
        if !ok{
            fmt.Println("感知到通道已經關閉")
            break
        }
        fmt.Println("接收方接收值:", elem)
    }
    fmt.Println("結束...")
}

最好不要讓接收方來關閉通道,而應當讓發送方來關閉。

單向通道

之前說的通道,指的都是雙向通道,就是既能發也能收。
單向通道,只能發不能收,或者只能收不能發的通道。定義單向通道的方法:

var c1 = make(chan<- int, 1)  // 只能發不能收
var c2 = make(<-chan int, 1)  // 只能收不能發

只能發不能收的通道,可以簡稱爲發送通道
只能收不能發的通道,可以簡稱爲接收通道

單向通道約束函數行爲

通過單向通道可以約束其他代碼的行爲。下面是對之前的一個例子稍加修改:

package main

import "fmt"

func sender(ch chan<- int) {
    for i := 0; i < 10; i++ {
        fmt.Println("發送方發送值:", i)
        ch <- i
    }
    fmt.Println("發送完畢,關閉通道...")
    close(ch)
}

func reveiver(ch <-chan int) {
    for {
        elem, ok := <- ch
        if !ok {
            fmt.Println("感知到通道已經關閉")
            break
        }
        fmt.Println("接收方接收值:", elem)
    }
}

func main() {
    intChan := make(chan int, 2)
    // 發送方
    go sender(intChan)
    // 接收方
    reveiver(intChan)
    fmt.Println("結束...")
}

這裏把之前主函數裏的發送方和接收方的代碼都封裝了一個函數中去了。在函數內部只接收單向通道,這樣在封裝函數內部,就只能對該通道進行定義的單向操作。在調用函數的時候,仍然是把雙向通道傳給它。Go語言會自動把雙向通道轉換爲函數所需的單向通道。這樣在函數內部該通道就是單向的,但是在函數外部就沒有限制。

單向通道約束調用方

下面的例子中定義了函數getIntChan,該函數會返回一個 <-chan int 類型的通道。得到該通道的程序只能從通道中接收元素。這是對函數調用方的一種約束:

package main

import "fmt"

func getIntChan() <-chan int {
    num := 5
    ch := make(chan int, num)
    for i := 0; i < num; i++ {
        ch <- i
    }
    close(ch)
    return ch
}

func main() {
    intChan := getIntChan()
    for elem := range intChan {
        fmt.Println(elem)
    }
}

上面用了for range來遍歷通道中的所有元素。

select語句

我們可以使用帶range子句的for語句從通道中獲取數據,也可以通過select語句操縱通道。

示例

select語句只能與通道聯用,它一般由若干個分支組成。由於select語句是專爲通道而設計的,所以每個case表達式中都只能包含操作通道的表達式,比如接收表達式。如果還需要把接收表達式賦值給變量的話,可以寫成賦值語句或短變量聲明:

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func init() {
    rand.Seed(time.Now().UnixNano())
}

func main() {
    // 生成了包括3個通道的數組
    intChannels := [3]chan int{
        make(chan int, 1),
        make(chan int, 1),
        make(chan int, 1),
    }
    // 生成返範圍在[0,2]的隨機數,效果就是隨機選擇一個通道
    index := rand.Intn(3)
    fmt.Println("index:", index)
    intChannels[index] <- index
    // 哪個通道中遠元素可以取出,那個對應的分支就會被執行
    select {
    case <-intChannels[0]:
        fmt.Println("選中了第一個通道")
    case <-intChannels[1]:
        fmt.Println("選中了第二個通道")
    case elem := <- intChannels[2]:
        fmt.Println("選中了第三個通道,值:", elem)
    default:  // 這個默認分支不會被選中
        fmt.Println("沒有選中任何通道")
    }
}

使用select語句的注意點

使用select語句,需要注意一下幾點:

  1. 如果加入了默認分支,select語句就不會被阻塞。如果所有通道的表達式都阻塞了,那麼就會執行默認分支
  2. 如果沒有加入默認分支,那一旦所有的表達式都沒有滿足求職條件,select就會阻塞。知道至少有一個case表達式滿足條件
  3. 可能會因爲通道關閉了,而直接從通道接到到一個元素類型的零值。這時候就需要通過表達式的第二個返回值來判斷通道是否關閉,一旦發現某個通道關閉了,就應該及時屏蔽掉對應的分支或者採取別的措施。
  4. select語句只能對其中的每一個case表達式各求值一次。如果想連續的或是定時的操作其中的通道,需要通過for循環嵌入select來實現。這樣的話要主要,簡單的在select裏使用break,只能結束當前的select,不會退出for循環。

下面是通過表達式的第二個返回值判斷通道關閉的示例:

package main

import (
    "fmt"
    "time"
)

func main() {
    intChan := make(chan int, 1)
    // 3秒後關閉通道
    time.AfterFunc(time.Second * 3, func() {
        close(intChan)
    })
    select {
    case _, ok := <- intChan:
        if !ok {
            fmt.Println("通道已經關閉")
            break
        }
        fmt.Println("通道里有值傳入")
    }
}

select語句的分支選擇的規則

規則如下所示:

  1. 當case表達式被求值是,如果包含多個表達式,總會按照從左到有的順序被求職
  2. 所有case表達式都會被求職,並且是按從上到下的順序。結合上面一條,就是先從左到右,再從上到下對所有的表達式求職
  3. 如果是發送表達式或者接受表達式,在被求值時處於阻塞狀態就認爲是求職不成功,也就是case表達式所在的分支不滿足選擇條件
  4. 僅當select語句裏所有的case表達式都被求職完畢後,纔會開始選擇候選分支。如果所有分支都不滿足,那就選默認分支。如果沒有默認分支,就阻塞,直到至少有一個候選分支返回條件爲止。
  5. 如果有多個滿足條件的分支,那麼會有一種僞隨機算法選擇其中一個分支並執行。
  6. 一條select語句只能由一個默認分支。默認分支只在無候選分支是纔會被執行,與它的編寫位置無關。
  7. select語句的每次執行,包括case表達式求值和分支選擇,都是獨立的。但是,不是併發安全的,具體要看其中的代碼是否是併發安全了。

下面是驗證上述規則的示例:

package main

import "fmt"

var channels = [3]chan int{
    nil,
    make(chan int),
    nil,
}

var numbers = []int{1, 2, 3}

func main() {
    select {
    case getChan(0) <- getNumber(0):
        fmt.Println("The first candidate case is selected.")
    case getChan(1) <- getNumber(1):
        fmt.Println("The second candidate case is selected.")
    case getChan(2) <- getNumber(2):
        fmt.Println("The third candidate case is selected")
    default:
        fmt.Println("No candidate case is selected!")
    }
}

func getNumber(i int) int {
    fmt.Printf("numbers[%d]\n", i)
    return numbers[i]
}

func getChan(i int) chan int {
    fmt.Printf("channels[%d]\n", i)
    return channels[i]
}
/* 執行結果
PS H:\Go\src\Go36\article11\example05> go run main.go
channels[0]
numbers[0]
channels[1]
numbers[1]
channels[2]
numbers[2]
No candidate case is selected!
PS H:\Go\src\Go36\article11\example05>
*/
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章