介紹 Golang 通道(channel)

介紹 Golang 通道(channel)

本文介紹如何使用Golang通道。通道是Go應用中鏈接協程通信的管道,協程可以往通道中推入值或從中讀取值。利用通道可以非常方便地實現高性能、高併發應用,相比與其他語言更簡單,這並不是巧合,而是Go語言的設計理念————併發作爲語言的一等公民,使得併發應用盡可能簡單而不失靈活性。

通道的思想起始很早就有了,但Go的實現者希望通道承擔更多使命————以儘可能簡單的方式讓開發者創建更好、更清晰的高性能併發應用。確實,Go語言最吸引我的能力是可以輕鬆創建併發應用,到目前爲止,不得不說這是一種樂趣。

1. 從簡單示例開始

我們首先從簡單示例開始。創建一個函數負責計算生成一個隨機數並傳給通道:

package main

import (
    "fmt"
    "math/rand"
)

func CalculateValue(values chan int) {
    // 設置隨機種子,避免隨機函數生成相同的值
    rand.Seed(time.Now().UnixNano()) 
    value := rand.Intn(10)

    fmt.Println("計算隨機值: {}", value)

    // 往通道發送值
    values <- value
}

func main() {
    fmt.Println("Golang Channel 教程")

    // 創建int類型通道,只能傳入int類型值  
    values := make(chan int)
    defer close(values)

    go CalculateValue(values)

    // 從通道接收值
    value := <-values
    fmt.Println(value)
}

我們分析下上面的程序。main函數中調用values := make(chan int) 新建通道變量values,後續作爲參數傳入CalculateValue函數。

注:使用make實例化通道,和map、slice一樣,channel使用前必須先用make實例化。

創建通道之後,接着調用defer close(values) ,確保main函數完成之前調用關閉方法。這種方法通常被認爲最佳實踐,讓代碼更整潔。
接着啓動協程CalculateValue(values),新建的通道作爲函數參數。函數內部計算一個值(1~10),打印結果併發送給通道。

回到main函數中,調用value := <-values從通道中接收值並打印。

注:當我們執行該程序時,程序並沒有立刻結束。這是因爲給通道發送值或從通道接收值都會阻塞當前協程。main函數被阻塞直到從通道中接收到值爲止。

執行代碼,查看運行結果:

Go Channel Tutorial
Calculated Random Value: {} 1
1

我們看到實例化並使用通道非常直接、簡單,下面繼續看稍微複雜的場景。

2. 無緩衝通道

雖然通道非常方便,但有時在協程中使用通道會導致問題行爲,與預期不一致。默認無緩存通道,當一個協程給其發送一個值,該協程會被阻塞,直到通道中的值被接收。
讓我們看一個真實示例,代碼與上節示例類似,但是我們擴展CalculateValue()函數在在發送值給通道之後增加一行輸出語句。

在main函數中增加一句go CalculateValue(valueChannel),我們期望兩個值發往通道。

package main

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

func CalculateValue(c chan int) {
    // 設置隨機種子,避免隨機函數生成相同的值
    rand.Seed(time.Now().UnixNano()) 
    value := rand.Intn(10)
    fmt.Println("計算隨機值: {}", value)
    time.Sleep(1000 * time.Millisecond)
    c <- value
    fmt.Println("僅當另一個協程執行從通道中取值之後才執行")
}

func main() {
    fmt.Println("Golang Channel 教程")

    // 創建int類型通道,只能傳入int類型值  
    valueChannel := make(chan int)
    defer close(valueChannel)

    go CalculateValue(valueChannel)
    go CalculateValue(valueChannel)

    // 從通道接收值
    values := <-valueChannel
    fmt.Println(values)
}

執行程序,我們僅看到第一個協程執行了最後一行輸出語句:

Golang Channel 教程
計算隨機值: {} 4
僅當另一個協程執行從通道中取值之後才執行
4

這是在第二個協程中調用 c <- value語句阻塞了,後續main函數在第二個協程執行之前提前結束。

3. 緩存通道

繞過這種阻塞行爲的方法是使用緩衝通道。緩存通道本質是使用給定大小的隊列實現跨協程通信。創建緩存通道需要make函數中指定容量:

bufferedChannel := make(chan int, 2)

通過指定容量創建緩存通道,當協程再執行 c <- value時只有緩存通道滿了纔會阻塞。

我們修改上面的程序使用緩存通道,同時在main函數中增加等待語句,確保main函數最後完成。

func CalculateValue(c chan int) {
	// 設置隨機種子,避免隨機函數生成相同的值
	rand.Seed(time.Now().UnixNano())
	value := rand.Intn(10)
	fmt.Println("計算隨機值: {}", value)
	time.Sleep(1000 * time.Millisecond)
	c <- value
	fmt.Println("僅當另一個協程執行從通道中取值之後才執行")
}

func main() {
	fmt.Println("Golang Channel 教程")

	// 創建int類型通道,只能傳入int類型值
	valueChannel := make(chan int, 2)
	defer close(valueChannel)

	go CalculateValue(valueChannel)
	go CalculateValue(valueChannel)

	// 從通道接收值
	values := <-valueChannel
	fmt.Println(values)
	time.Sleep(1000 * time.Millisecond)
}

現在執行程序應該能夠看到第二個協程繼續執行,無論通道中的值是否被接收。通過對比我們看到兩種通道的差異:非緩存通道默認阻塞,而緩存通道當沒有滿時不阻塞。
輸出結果:

Golang Channel 教程
計算隨機值: {} 6
僅當另一個協程執行從通道中取值之後才執行
6
僅當另一個協程執行從通道中取值之後才執行

4. 總結

本文我們討論了Golang的通道,瞭解兩種通道的差異以及如何在Golang併發應用中使用。

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