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
- 接收(資料從通道中流出)
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”。