Go_Channel詳解

一 channel介紹

單純地將函數並發執行是沒有意義的。函數與函數間需要交換數據才能體現並發執行函數的意義。

雖然可以使用共享記憶體進行數據交換,但是共享記憶體在不同的goroutine中容易發生競態問題。為了保證數據交換的正確性,必須使用互斥量對記憶體進行加鎖,這種做法勢必造成性能問題。

Go語言的並發模型是CSP(Communicating Sequential Processes),提倡通過通訊共享記憶體而不是通過共享記憶體而實現通訊。

如果說goroutine是Go程式並發的執行體,channel就是它們之間的連接。channel是可以讓一個goroutine發送特定值到另一個goroutine的通訊機制。

Go 語言中的通道(channel)是一種特殊的類型。通道像一個傳送帶或者隊列,總是遵循先入先出(First In First Out)的規則,保證收發數據的順序。每一個通道都是一個具體類型的導管,也就是聲明channel的時候需要為其指定元素類型。

二 channel類型

channel是一種類型,一種引用類型。聲明通道類型的格式如下:

    var 變數 chan 元素類型

舉幾個例子:

    var ch1 chan int   // 聲明一個傳遞整型的通道
    var ch2 chan bool  // 聲明一個傳遞布爾型的通道
    var ch3 chan []int // 聲明一個傳遞int切片的通道

三 創建channel

通道是引用類型,通道類型的空值是nil。

var ch chan int
fmt.Println(ch) // <nil>

聲明的通道後需要使用make函數初始化之後才能使用。

創建channel的格式如下:

make(chan 元素類型, [緩衝大小])

channel的緩衝大小是可選的。

舉幾個例子:

ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)

四 channel操作

通道有發送(send)、接收(receive)和關閉(close)三種操作。

發送和接收都使用<-符號。

現在我們先使用以下語句定義一個通道:

ch := make(chan int)

發送

將一個值發送到通道中。

ch <- 10 // 把10發送到ch中

接收

從一個通道中接收值。

x := <- ch // 從ch中接收值並賦值給變數x
<-ch       // 從ch中接收值,忽略結果

關閉

我們通過調用內置的close函數來關閉通道。

close(ch)

關於關閉通道需要注意的事情是,只有在通知接收方goroutine所有的數據都發送完畢的時候才需要關閉通道。通道是可以被垃圾回收機制回收的,它和關閉文件是不一樣的,在結束操作之後關閉文件是必須要做的,但關閉通道不是必須的。

關閉後的通道有以下特點:

1.對一個關閉的通道再發送值就會導致panic。
2.對一個關閉的通道進行接收會一直獲取值直到通道為空。
3.對一個關閉的並且沒有值的通道執行接收操作會得到對應類型的零值。
4.關閉一個已經關閉的通道會導致panic。

五 無緩衝的通道

在这里插入图片描述
無緩衝的通道又稱為阻塞的通道。我們來看一下下面的程式碼:

func main() {
    ch := make(chan int)
    ch <- 10
    fmt.Println("發送成功")
}

上面這段程式碼能夠通過編譯,但是執行的時候會出現以下錯誤:

 fatal error: all goroutines are asleep - deadlock!

    goroutine 1 [chan send]:
    main.main()
            .../src/github.com/pprof/studygo/day06/channel02/main.go:8 +0x54

為什麼會出現deadlock錯誤呢?

因為我們使用ch := make(chan int)創建的是無緩衝的通道,無緩衝的通道只有在有人接收值的時候才能發送值。就像你住的小區沒有快遞櫃和代收點,快遞員給你打電話必須要把這個物品送到你的手中,簡單來說就是無緩衝的通道必須有接收才能發送。

上面的程式碼會阻塞在ch <- 10這一行程式碼形成死鎖,那如何解決這個問題呢?

一種方法是啟用一個goroutine去接收值,例如:

func recv(c chan int) {
    ret := <-c
    fmt.Println("接收成功", ret)
}
func main() {
    ch := make(chan int)
    go recv(ch) // 啟用goroutine從通道接收值
    ch <- 10
    fmt.Println("發送成功")
}

無緩衝通道上的發送操作會阻塞,直到另一個goroutine在該通道上執行接收操作,這時值才能發送成功,兩個goroutine將繼續執行。相反,如果接收操作先執行,接收方的goroutine將阻塞,直到另一個goroutine在該通道上發送一個值。

使用無緩衝通道進行通訊將導致發送和接收的goroutine同步化。因此,無緩衝通道也被稱為同步通道。

六 有緩衝的通道

解決上面問題的方法還有一種就是使用有緩衝區的通道。
在这里插入图片描述
我們可以在使用make函數初始化通道的時候為其指定通道的容量,例如:

func main() {
    ch := make(chan int, 1) // 創建一個容量為1的有緩衝區通道
    ch <- 10
    fmt.Println("發送成功")
}

只要通道的容量大於零,那麼該通道就是有緩衝的通道,通道的容量表示通道中能存放元素的數量。就像你小區的快遞櫃只有那麼個多格子,格子滿了就裝不下了,就阻塞了,等到別人取走一個快遞員就能往裡面放一個。

我們可以使用內置的len函數獲取通道內元素的數量,使用cap函數獲取通道的容量,雖然我們很少會這麼做。

七 close()

可以通過內置的close()函數關閉channel(如果你的管道不往裡存值或者取值的時候一定記得關閉管道)

package main

import "fmt"

func main() {
    c := make(chan int)
    go func() {
        for i := 0; i < 5; i++ {
            c <- i
        }
        close(c)
    }()
    for {
        if data, ok := <-c; ok {
            fmt.Println(data)
        } else {
            break
        }
    }
    fmt.Println("main結束")
}

八 通道總結

channel常見的異常總結,如下圖:
在这里插入图片描述
注意:關閉已經關閉的channel也會引發panic。

Tags: