1. 程式人生 > >Go語言實戰-- 通道

Go語言實戰-- 通道

上一篇我們講的原子函式和互斥鎖,都可以保證共享資料的讀寫,但是呢,它們還是有點複雜,而且影響效能,對此,Go又為我們提供了一種工具,這就是通道。

所以在多個goroutine併發中,我們不僅可以通過原子函式和互斥鎖保證對共享資源的安全訪問,消除競爭的狀態,還可以通過使用通道,在多個goroutine傳送和接受共享的資料,達到資料同步的目的。

通道,他有點像在兩個routine之間架設的管道,一個goroutine可以往這個管道里塞資料,另外一個可以從這個管道里取資料,有點類似於我們說的佇列。

宣告一個通道很簡單,我們使用 chan 關鍵字即可,除此之外,還要指定通道中傳送和接收資料的型別,這樣我們才能知道,要傳送什麼型別的資料給通道,也知道從這個通道里可以接收到什麼型別的資料。

ch:=make(chan int)

通道型別和Map這些型別一樣,可以使用內建的 make 函式宣告初始化,這裡我們初始化了一個 chan int 型別的通道,所以我們只能往這個通道里傳送 int 型別的資料,當然接收也只能是 int 型別的資料。

我們知道,通道是用於在goroutine之間通訊的,它具有傳送和接收兩個操作,而且這兩個操作的運算子都是 <- 。

ch <- 2 //傳送數值2給這個通道
x:=<-ch //從通道里讀取值,並把讀取的值賦值給x變數
<-ch //從通道里讀取值,然後忽略

看例子,慢慢理解發送和接收的用法。傳送操作 <- 在通道的後面,看箭頭方向,表示把數值2傳送到通道 ch

 裡;接收操作 <- 在通道的前面,而且是一個一元操作符,看箭頭方向,表示從通道 ch 裡讀取資料。讀取的資料可以賦值給一個變數,也可以忽略。

通道我們還可以使用內建的 close 函式關閉。

close(ch)

如果一個通道被關閉了,我們就不能往這個通道里傳送資料了,如果傳送的話,會引起 painc異常。但是,我們還可以接收通道里的資料,如果通道里沒有資料的話,接收的資料是 nil 。

剛剛我們使用 make 函式初始化的時候,只有一個引數,其實 make 還可以有第二個引數,用於指定通道的大小。預設沒有第二個引數的時候,通道的大小為0,這種通道也被成為 無緩衝通道

ch:=make
(chan int) ch:=make(chan int,0) ch:=make(chan int,2)

看例子,其中第一個和第二個初始化是等價的。第三個初始化建立了一個大小為2的通道,這種稱為 有緩衝通道 。

無緩衝的通道

無緩衝的通道指的是通道的大小為0,也就是說,這種型別的通道在接收前沒有能力儲存任何值,它要求傳送goroutine和接收goroutine同時準備好,才可以完成傳送和接收操作。

從上面無緩衝的通道定義來看,傳送goroutine和接收gouroutine必須是同步的,同時準備後,如果沒有同時準備好的話,先執行的操作就會阻塞等待,直到另一個相對應的操作準備好為止。這種無緩衝的通道我們也稱之為 同步通道 。

funcmain() {
	ch := make(chan int)

	go func() {
		var sum int = 0
		for i := 0; i < 10; i++ {
			sum += i
		}
		ch <- sum
	}()
	
	fmt.Println(<-ch)

}

在前面的例子中,我們為了演示goroutine,防止程式提前終止,都是使用 sync.WaitGroup 進行等待,現在的這個例子就不用了,我們使用同步通道來等待。

在計算sum和的goroutine沒有執行完,把值賦給 ch 通道之前, fmt.Println(<-ch) 會一直等待,所以 main 主goroutine就不會終止,只有當計算和的goroutine完成後,並且傳送到 ch 通道的操作準備好後,同時 <-ch 就會接收計算好的值,然後打印出來。

管道

我們在使用Bash的時候,有個管道操作 | ,它的意思是把上一個操作的輸出,當成下一個操作的輸入,連起來,做一連串的處理操作。

➜  ~ ls |grep 'D'  
Desktop
Documents
Downloads

比如上面這個例子的意思是,先使用 ls 命令,把當前目錄下的目錄和檔案列出來,作為下一個 grep 命令的輸入,然後通過 grep 命令,匹配我們需要顯示的目錄和檔案,這裡匹配以 D開頭的檔名或者目錄名。

其實我們使用通道也可以做到管道的效果,我們只需要把一個通道的輸出,當成下一個通道的輸入即可。

funcmain() {
	one := make(chan int)
	two := make(chan int)

	go func() {
		one<-100
	}()

	go func() {
		v:=<-one
		two<-v
	}()

	fmt.Println(<-two)

}

這裡例子中我們定義兩個通道 one 和 two ,然後按照順序,先把100傳送給通道 one ,然後用另外一個goroutine從 one 接收值,再發送給通道 two ,最終在主goroutine裡等著接收列印 two 通道里的值,這就類似於一個管道的操作,把通道 one 的輸出,當成通道 two 的輸入,類似於接力賽一樣。

有緩衝的通道

有緩衝通道,其實是一個佇列,這個佇列的最大容量就是我們使用 make 函式建立通道時,通過第二個引數指定的。

ch := make(chan int, 3)

這裡建立容量為3的,有緩衝的通道。對於有緩衝的通道,向其傳送操作就是向佇列的尾部插入元素,接收操作則是從佇列的頭部刪除元素,並返回這個剛剛刪除的元素。

當佇列滿的時候,傳送操作會阻塞;當佇列空的時候,接受操作會阻塞。有緩衝的通道,不要求傳送和接收操作時同步的,相反可以解耦傳送和接收操作。

想知道通道的容量以及裡面有幾個元素資料怎麼辦?其實和 map 一樣,使用 cap 和 len 函式就可以了。

cap(ch)
len(ch)

cap 函式返回通道的最大容量, len 函式返回現在通道里有幾個元素。

funcmirroredQuery()string {
    responses := make(chan string, 3)
    go func() { responses <- request("asia.gopl.io") }()
    go func() { responses <- request("europe.gopl.io") }()
    go func() { responses <- request("americas.gopl.io") }()
    return <-responses // return the quickest response
}
funcrequest(hostnamestring)(responsestring) { /* ... */ }

這是Go語言聖經裡比較有意義的一個例子,例子是想獲取服務端的一個數據,不過這個資料在三個映象站點上都存在,這三個映象分散在不同的地理位置,而我們的目的又是想最快的獲取到資料。

所以這裡,我們定義了一個容量為3的通道 responses ,然後同時發起3個併發goroutine向這三個映象獲取資料,獲取到的資料傳送到通道 responses 中,最後我們使用 return <-responses 返回獲取到的第一個資料,也就是最快返回的那個映象的資料。

單向通道

有時候,我們有一些特殊場景,比如限制一個通道只可以接收,但是不能傳送;有時候限制一個通道只能傳送,但是不能接收,這種通道我們稱為單向通道。

定義單向通道也很簡單,只需要在定義的時候,帶上 <- 即可。

var send chan<- int //只能傳送
var receive <-chan int //只能接收

注意 <- 操作符的為止,在後面是隻能傳送,對應傳送操作;在前面是隻能接收,對應接收操作。

單向通道應用於函式或者方法的引數比較多,比如

funccounter(outchan<-int) {
}

例子這樣的,只能進行傳送操作,防止誤操作,使用了接收操作,如果使用了接收操作,在編譯的時候就會報錯的。

使用通道可以很簡單的在goroutine之間共享資料,下一篇會具體介紹一些例子,以便更好的理解併發。