1. 程式人生 > >Go 通道(channel)與協程間通訊

Go 通道(channel)與協程間通訊

協程間通訊

協程中可以使用共享變數來通訊,但是很不提倡這樣做,因為這種方式給所有的共享記憶體的多執行緒都帶來了困難。

在 Go 中有一種特殊的型別,通道(channel),就像一個可以用於傳送型別化資料的管道,由其負責協程之間的通訊,從而避開所有由共享記憶體導致的陷阱;這種通過通道進行通訊的方式保證了同步性。

資料在通道中進行傳遞:在任何給定時間,一個數據被設計為只有一個協程可以對其訪問,所以不會發生資料競爭。資料的所有權(可以讀寫資料的能力)也因此被傳遞。

通道服務於通訊的兩個目的:值的交換,同步的,保證了兩個計算(協程)任何時候都是可知狀態。

宣告與初始化

通道的宣告格式如下:

var identifier chan datatype

未初始化的通道的值為 nil。
從宣告的格式能夠看出來,通道只能傳輸一種型別的資料,比如 chan int 或者 chan string,所有的型別都可以用於通道,空介面 interface{} 也可以。

通道也是引用型別,所以我們使用 make() 函式來給它分配記憶體。

var ch1 chan string
ch1 = make(chan string)
//或者簡寫為
ch2 := make(chan string)

通訊操作符

操作符 <- 直觀的表示了資料的傳輸,資訊按照箭頭的方向流動。

  • 傳送(資料流向通道)
    ch <- int1
    表示:將變數 int1 放入通道中,用通道 ch 傳送變數 int1
  • 接收(資料從通道中流出)
    int2 = <- ch 表示:變數 int2 從通道 ch 接收資料(獲取新值)。

下面的例子展示了兩個協程之間的通訊:

import (
   "fmt"
   "time"
)

func main() {
   ch := make(chan string)

   go sendData(ch)
   go getData(ch)

   time.Sleep(1e9)
}

func sendData(ch chan string){
  ch <- "golang"
}

func getData(ch chan string){
  fmt.Println(<- ch)
}

輸出結果:

在 main() 方法的最後一行中,使用了 time 包中的 sleep 函式來暫停1秒,以確保 main() 方法會在另個兩個協程之後結束,如果不在 main() 方法中等待,協程會隨著程式的結束而消亡。

通道阻塞

預設情況下,通訊是同步且無緩衝的,通道的傳送/接收操作在對方準備好之前都是阻塞的:

  • 對於同一個通道,在沒有接受者接收資料之前,傳送操作會被阻塞。
  • 對於同一個通道,在沒有傳送者傳送資料之前,接收操作會被阻塞。

現在我們把上面的例子修改一下,去掉 sendData() 方法前的 go 關鍵字:

func main() {
    ch := make(chan string)
    
    //go sendData(ch)
    sendData(ch)
    go getData(ch)
    
    time.Sleep(1e9)
}

輸出結果:

執行程式後出錯了,丟擲了一個 panic,這是為什麼呢?

這是因為 Go 程式在執行時會檢查所有的協程,查詢是否存在有阻塞(讀取或者寫入某個通道)的情況。

而上面這段程式碼中的 sendData() 方法阻塞了 main() 方法,導致 go getData()無法執行,也就是說通道的接收操作也就無法被執行,而 sendData() 中的傳送操作也會一直等待,這就導致程式無法繼續執行。這就是死鎖(deadlock)形式。

如果我們接著再修改一下程式碼,保留 sendData() 方法的關鍵字,而去掉 getData() 方法的關鍵字:

func main() {
  ch := make(chan string)

   go sendData(ch)
   getData(ch)

   time.Sleep(1e9)
}

因為傳送和接收操作都會被執行,所以結果是正常輸出“golang”。