Go語言中通道(channel)用於goroutine通訊

2020-09-25 12:01:01

通道是用來傳遞資料的一個資料結構。從設計上確保,在同一時刻只有一個 goroutine 能從中接收或放入資料。傳送和接收都是原子操作,不會中斷。

Go語言中的通道(channel)是一種特殊的型別。在任何時候,同時只能有一個 goroutine 存取通道進行傳送和獲取資料。goroutine 間通過通道就可以通訊。

通道可用於兩個 goroutine 之間通過傳遞一個指定型別的值來同步執行和通訊。操作符 <- 用於指定通道的方向,傳送或接收。如果未指定方向,則為雙向通道。

ch <- v    // 把 v 傳送到通道 ch

v := <-ch  // 從 ch 接收資料,並把值賦給 v          

宣告通道型別

通道本身需要一個型別進行修飾,就像切片型別需要標識元素型別。通道的元素型別就是在其內部傳輸的資料型別,宣告如下:

var 通道變數 chan 通道型別

chan 型別的空值是 nil,宣告後需要配合 make 後才能使用。

建立通道

宣告一個通道,使用chan關鍵字即可,通道在使用前必須先建立,通道是參照型別,需要使用 make 進行建立:

通道範例 := make(chan 資料型別)

  • 資料型別:通道內傳輸的元素型別。
  • 通道範例:通過make建立的通道控制程式碼。

範例:

ch1 := make(chan int)                 // 建立一個整型型別的通道
ch2 := make(chan interface{})         // 建立一個空介面型別的通道, 可以存放任意格式

type Equip struct{ /* 欄位 */ }
ch2 := make(chan *Equip)             // 建立Equip指標型別的通道, 可以存放*Equip

goroutine

Go 語言支援並行,我們只需要通過 go 關鍵字來開啟 goroutine 即可。goroutine 是輕量級執行緒,goroutine 的排程是由 Golang 執行時進行管理的。

Go 允許使用 go 語句開啟一個新的執行期執行緒, 即 goroutine,以一個不同的、新建立的 goroutine 來執行一個函數。 同一個程式中的所有 goroutine 共用同一個地址空間。

例如:

go a(x, y, z)

這樣就開啟了一個新的 goroutine。

範例:

func say(s string) {
    for i := 0; i < 5; i++ {
            time.Sleep(100 * time.Millisecond)
            fmt.Println(s)
    }
}

func main() {
    go say("world")
    say("hello")
}

sync.waitGroup
等待所有goroutine執行完成,並且阻塞主執行緒的執行,直到所有的goroutine執行完成。

GOMAXPROCS
呼叫 runtime.GOMAXPROCS() 用來設定可以平行計算的CPU核數的最大值,並返回之前的值。

通道

如果說 goroutine 是 Go語言程式的並行體的話,那麼 channels 就是它們之間的通訊機制。一個 channels 是一個通訊機制,它可以讓一個 goroutine 通過它給另一個 goroutine 傳送值資訊。每個 channel 都有一個特殊的型別,也就是 channels 可傳送資料的型別。一個可以傳送 int 型別資料的 channel 一般寫為 chan int。

Go語言提倡使用通訊的方法代替共用記憶體,當一個資源需要在 goroutine 之間共用時,通道在 goroutine 之間架起了一個管道,並提供了確保同步交換資料的機制。
宣告通道時,需要指定將要被共用的資料的型別。可以通過通道共用內建型別、命名型別、結構型別和參照型別的值或者指標。

這裡通訊的方法就是使用通道(channel),如下圖所示:

在這裡插入圖片描述
在地鐵站、食堂、洗手間等公共場所人很多的情況下,大家養成了排隊的習慣,目的也是避免擁擠、插隊導致的低效的資源使用和交換過程。
程式碼與資料也是如此,多個 goroutine 為了爭搶資料,勢必造成執行的低效率,使用佇列的方式是最高效的,channel 就是一種佇列一樣的結構。

通道用途

  • 使用通道來同步goroutine
  • 使用非同步管道來保護臨界資源(令牌 誰拿到了才能用,使用完退回管道)
  • 用管道來傳送事件

同步通道

緩衝長度為0的channel稱為同步管道,可以用來同步兩個routine

  • 傳送操作被阻塞,直到接收端準備好接收
  • 接收操作被阻塞,直到傳送端準備好傳送

非同步通道

緩衝長度大於0的channel稱為非同步管道。非同步 channel,就是給 channel 設定個 buffer 值

  • 在 buffer 未填滿的情況下 ,不阻塞傳送操作。
  • 在buffer 未讀完前,不阻塞接收操作。

通過 range 關鍵字來實現遍歷讀取到的資料,類似於與陣列或切片。格式如下:

v, ok := <-ch

如果通道接收不到資料後 ok 就為 false,這時通道就可以使用 close() 函數來關閉。

範例:

    // 宣告通道    
    ch := make(chan int)
	
    var ch1 chan int       // ch1是一個正常的channel,不是單向的
    var ch2 chan<- float64 // ch2是單向channel,只用於寫float64資料
    var ch3 <-chan int     // ch3是單向channel,只用於讀取int資料
	
	// 通道可以設定緩衝區,通過 make 的第二個引數指定緩衝區大小
	
	ch := make(chan int, 2)
	
	// 因為 ch 是帶緩衝的通道,我們可以同時傳送兩個資料
	// 而不用立刻需要去同步讀取資料
	ch <- 1
	ch <- 2
	
	// 獲取這兩個資料
	fmt.Println(<-ch)
    fmt.Println(<-ch)

func fibonacci(n int, c chan int) {
    x, y := 0, 1
    for i := 0; i < n; i++ {
            c <- x
            x, y = y, x+y
    }
    close(c)
}


func main() {
    c := make(chan int, 10)
    go fibonacci(cap(c), c)
    // range 函數遍歷每個從通道接收到的資料,因為 c 在傳送完10個資料之後就關閉了通道。
    // 所以這裡我們 range 函數在接收到 10 個資料之後就結束了。
    // 如果上面的 c 通道不關閉,那麼 range 函數就不會結束,從而在接收第 11 個資料的時候就阻塞了。
 
    for i := range c {
            fmt.Println(i)
    }
}

執行結果為:

0
1
1
2
3
5
8
13
21
34

通道緩衝區

預設情況下,通道是不帶緩衝區的。傳送端傳送資料,同時必須有接收端相應的接收資料。

通道可以設定緩衝區,通過 make 的第二個引數指定緩衝區大小:

ch := make(chan int, 100)

帶緩衝區的通道允許傳送端的資料傳送和接收端的資料獲取處於非同步狀態,就是說傳送端傳送的資料可以放在緩衝區裡面,可以等待接收端去獲取資料,而不是立刻需要接收端去獲取資料。

不過由於緩衝區的大小是有限的,所以還是必須有接收端來接收資料的,否則緩衝區一滿,資料傳送端就無法再傳送資料了。

注意:
如果通道不帶緩衝,傳送方會阻塞直到接收方從通道中接收了值。如果通道帶緩衝,傳送方則會阻塞直到傳送的值被拷貝到緩衝區內;
如果緩衝區已滿,則意味著需要等待直到某個接收方獲取到一個值。接收方在有值可以接收之前會一直阻塞。

範例:

package main



import "fmt"



func main() {

        //定義了一個可以儲存整數型別的帶緩衝通道
        // 緩衝區大小為2

        ch := make(chan int, 2)


        // 因為 ch 是帶緩衝的通道,我們可以同時傳送兩個資料
        // 而不用立刻需要去同步讀取資料

        ch <- 1

        ch <- 2


        // 獲取這兩個資料

        fmt.Println(<-ch)

        fmt.Println(<-ch)

}

執行結果為:

1
2