學習筆記-通道

Rob Pike 的至理名言:Don’t communicate by sharing memory; share memory by communicating. (不要通過共享內存來通信,而應該通過通信來共享內存。)

容量,就是指通道最多可以緩存多少個元素值,當容量爲0時,我們可以稱通道爲非緩衝通道,也就是不帶緩衝的通道。而當容量大於0時,我們可以稱爲緩衝通道,也就是帶有緩衝的通道。

在聲明並初始化一個通道的時候,我們需要用到 Go 語言的內建函數make,在聲明一個通道類型變量的時候,我們首先要確定該通道類型的元素類型,比如,類型字面量chan int,其中的chan是表示通道類型的關鍵字,而int則說明了該通道類型的元素類型。又比如,chan string代表了一個元素類型爲string的通道類型。

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

在 demo20.go 文件中,我聲明並初始化了一個元素類型爲int、容量爲3的通道ch1,並用三條語句,向該通道先後發送了三個元素值2、1和3。這裏的語句需要這樣寫:
依次敲入通道變量的名稱(比如ch1)、接送操作符<-以及想要發送的元素值(比如2),並且這三者之間最好用空格進行分割。

當我們需要從通道接收元素值的時候,同樣要用接送操作符<-,只不過,這時需要把它寫在變量名的左邊,用於表達“要從該通道接收一個元素值”的語義。

比如:<-ch1,這也可以被叫做接收表達式。在一般情況下,接收表達式的結果將會是通道中的一個元素值。

package main
import "fmt"
func main() {
    ch1 := make(chan int, 3)
    ch1 <- 2
    ch1 <- 1
    ch1 <- 3
    elem1 := <-ch1
    fmt.Printf("The first element received from channel ch1: %v\n",
        elem1)
}
go run demo20.go 
The first element received from channel ch1: 2

對通道的發送和接收操作都有哪些基本的特性?

它們的基本特性如下:
對於同一個通道,發送操作之間是互斥的,接收操作之間也是互斥的。
Go 語言的運行時系統(以下簡稱運行時系統)只會執行對同一個通道的任意個發送操作中的某一個。直到這個元素值被完全複製進該通道之後,其他針對該通道的發送操作纔可能被執行。類似的,在同一時刻,運行時系統也只會執行,對同一個通道的任意個接收操作中的某一個。直到這個元素值完全被移出該通道之後,其他針對該通道的接收操作纔可能被執行。即使這些操作是併發執行的也是如此。這裏所謂的併發執行,你可以這樣認爲,多個代碼塊分別在不同的 goroutine 之中,並有機會在同一個時間段內被執行。另外,對於通道中的同一個元素值來說,發送操作和接收操作之間也是互斥的。例如,雖然會出現,正在被複制進通道但還未複製完成的元素值,但是這時它絕不會被想接收它的一方看到和取走。

這裏要注意的一個細節是,元素值從外界進入通道時會被複制。更具體地說,進入通道的並不是在接收操作符右邊的那個元素值,而是它的副本。

另一方面,元素值從通道進入外界時會被移動。這個移動操作實際上包含了兩步,第一步是生成正在通道中的這個元素值的副本,並準備給到接收方,第二步是刪除在通道中的這個元素值。

發送操作和接收操作中對元素值的處理都是不可分割的。這裏的“不可分割”的意思是,它們處理元素值時都是一氣呵成的,絕不會被打斷。例如,發送操作要麼還沒複製元素值,要麼已經複製完畢,絕不會出現只複製了一部分的情況。接收操作在準備好元素值的副本之後,一定會刪除掉通道中的原值,絕不會出現通道中仍有殘留的情況。這既是爲了保證通道中元素值的完整性,也是爲了保證通道操作的唯一性。對於通道中的同一個元素值來說,它只可能是某一個發送操作放入的,同時也只可能被某一個接收操作取出。

發送操作在完全完成之前會被阻塞。接收操作也是如此。

一般情況下,發送操作包括了“複製元素值”和“放置副本到通道內部”這兩個步驟。在這兩個步驟完全完成之前,發起這個發送操作的那句代碼會一直阻塞在那裏。也就是說,在它之後的代碼不會有執行的機會,直到這句代碼的阻塞解除。更細緻地說,在通道完成發送操作之後,運行時系統會通知這句代碼所在的 goroutine,以使它去爭取繼續運行代碼的機會。另外,接收操作通常包含了“複製通道內的元素值”“放置副本到接收方”“刪掉原值”三個步驟。在所有這些步驟完全完成之前,發起該操作的代碼也會一直阻塞,直到該代碼所在的 goroutine 收到了運行時系統的通知並重新獲得運行機會爲止。說到這裏,你可能已經感覺到,如此阻塞代碼其實就是爲了實現操作的互斥和元素值的完整。

造成阻塞的情況

先說針對緩衝通道的情況。如果通道已滿,那麼對它的所有發送操作都會被阻塞,直到通道中有元素值被接收走。這時,通道會優先通知最早因此而等待的、那個發送操作所在的 goroutine,後者會再次執行發送操作。由於發送操作在這種情況下被阻塞後,它們所在的 goroutine 會順序地進入通道內部的發送等待隊列,所以通知的順序總是公平的。相對的,如果通道已空,那麼對它的所有接收操作都會被阻塞,直到通道中有新的元素值出現。這時,通道會通知最早等待的那個接收操作所在的 goroutine,並使它再次執行接收操作。因此而等待的、所有接收操作所在的 goroutine,都會按照先後順序被放入通道內部的接收等待隊列。對於非緩衝通道,情況要簡單一些。無論是發送操作還是接收操作,一開始執行就會被阻塞,直到配對的操作也開始執行,纔會繼續傳遞。由此可見,非緩衝通道是在用同步的方式傳遞數據。也就是說,只有收發雙方對接上了,數據纔會被傳遞。並且,數據是直接從發送方複製到接收方的,中間並不會用非緩衝通道做中轉。相比之下,緩衝通道則在用異步的方式傳遞數據。在大多數情況下,緩衝通道會作爲收發雙方的中間件。正如前文所述,元素值會先從發送方複製到緩衝通道,之後再由緩衝通道複製給接收方。但是,當發送操作在執行的時候發現空的通道中,正好有等待的接收操作,那麼它會直接把元素值複製給接收方。以上說的都是在正確使用通道的前提下會發生的事情。下面我特別說明一下,由於錯誤使用通道而造成的阻塞。對於值爲nil的通道,不論它的具體類型是什麼,對它的發送操作和接收操作都會永久地處於阻塞狀態。它們所屬的 goroutine 中的任何代碼,都不再會被執行。注意,由於通道類型是引用類型,所以它的零值就是nil。換句話說,當我們只聲明該類型的變量但沒有用make函數對它進行初始化時,該變量的值就會是nil。我們一定不要忘記初始化通道!

package main

func main() {
    // 示例1。
    ch1 := make(chan int, 1)
    ch1 <- 1
    //ch1 <- 2 // 通道已滿,因此這裏會造成阻塞。

    // 示例2。
    ch2 := make(chan int, 1)
    //elem, ok := <-ch2 // 通道已空,因此這裏會造成阻塞。
    //_, _ = elem, ok
    ch2 <- 1

    // 示例3。
    var ch3 chan int
    //ch3 <- 1 // 通道的值爲nil,因此這裏會造成永久的阻塞!
    //<-ch3 // 通道的值爲nil,因此這裏會造成永久的阻塞!
    _ = ch3
}

發送操作和接收操作在什麼時候會引發 panic?

對於一個已初始化,但並未關閉的通道來說,收發操作一定不會引發 panic。但是通道一旦關閉,再對它進行發送操作,就會引發 panic。
另外,如果我們試圖關閉一個已經關閉了的通道,也會引發 panic。注意,接收操作是可以感知到通道的關閉的,並能夠安全退出。更具體地說,當我們把接收表達式的結果同時賦給兩個變量時,第二個變量的類型就是一定bool類型。它的值如果爲false就說明通道已經關閉,並且再沒有元素值可取了。
注意,如果通道關閉時,裏面還有元素值未被取出,那麼接收表達式的第一個結果,仍會是通道中的某一個元素值,而第二個結果值一定會是true。
因此,通過接收表達式的第二個結果值,來判斷通道是否關閉是可能有延時的。由於通道的收發操作有上述特性,所以除非有特殊的保障措施,我們千萬不要讓接收方關閉通道,而應當讓發送方做這件事。這在 demo22.go 中有一個簡單的模式可供參考。

package main

import "fmt"

func main() {
    ch1 := make(chan int, 2)
    // 發送方。
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Printf("Sender: sending element %v...\n", i)
            ch1 <- i
        }
        fmt.Println("Sender: close the channel...")
        close(ch1)
    }()

    // 接收方。
    for {
        elem, ok := <-ch1
        if !ok {
            fmt.Println("Receiver: closed channel")
            break
        }
        fmt.Printf("Receiver: received an element: %v\n", elem)
    }

    fmt.Println("End.")
}

go run demo22.go
Sender: sending element 0...
Sender: sending element 1...
Sender: sending element 2...
Sender: sending element 3...
Receiver: received an element: 0
Receiver: received an element: 1
Receiver: received an element: 2
Receiver: received an element: 3
Sender: sending element 4...
Sender: sending element 5...
Receiver: received an element: 4
Sender: sending element 6...
Sender: sending element 7...
Sender: sending element 8...
Receiver: received an element: 5
Receiver: received an element: 6
Receiver: received an element: 7
Receiver: received an element: 8
Sender: sending element 9...
Sender: close the channel...
Receiver: received an element: 9
Receiver: closed channel
End.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章