這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
《Go語言實戰》讀書筆記,未完待續,歡迎掃碼關注公眾號flysnow_org,第一時間看後續筆記。覺得有協助的話,順手分享到朋友圈吧,感謝支援。
上一篇我們講的原子函數和互斥鎖,都可以保證共用資料的讀寫,但是呢,它們還是有點複雜,而且影響效能,對此,Go又為我們提供了一種工具,這就是通道。
所以在多個goroutine並發中,我們不僅可以通過原子函數和互斥鎖保證對共用資源的安全訪問,消除競爭的狀態,還可以通過使用通道,在多個goroutine發送和接受共用的資料,達到資料同步的目的。
通道,他有點像在兩個routine之間架設的管道,一個goroutine可以往這個管道裡塞資料,另外一個可以從這個管道裡取資料,有點類似於我們說的隊列。
聲明一個通道很簡單,我們使用chan關鍵字即可,除此之外,還要指定通道中發送和接收資料的類型,這樣我們才能知道,要發送什麼類型的資料給通道,也知道從這個通道裡可以接收到什麼類型的資料。
通道類型和Map這些類型一樣,可以使用內建的make函式宣告初始化,這裡我們初始化了一個chan int類型的通道,所以我們只能往這個通道裡發送int類型的資料,當然接收也只能是int類型的資料。
我們知道,通道是用於在goroutine之間通訊的,它具有發送和接收兩個操作,而且這兩個操作的運算子都是<-。
123 |
ch <- 2 //發送數值2給這個通道x:=<-ch //從通道裡讀取值,並把讀取的值賦值給x變數<-ch //從通道裡讀取值,然後忽略 |
看例子,慢慢理解發送和接收的用法。發送操作<-在通道的後面,看箭頭方向,表示把數值2發送到通道ch裡;接收操作<-在通道的前面,而且是一個一元操作符,看箭頭方向,表示從通道ch裡讀取資料。讀取的資料可以賦值給一個變數,也可以忽略。
通道我們還可以使用內建的close函數關閉。
如果一個通道被關閉了,我們就不能往這個通道裡發送資料了,如果發送的話,會引起painc異常。但是,我們還可以接收通道裡的資料,如果通道裡沒有資料的話,接收的資料是nil。
剛剛我們使用make函數初始化的時候,只有一個參數,其實make還可以有第二個參數,用於指定通道的大小。預設沒有第二個參數的時候,通道的大小為0,這種通道也被成為無緩衝通道。
123 |
ch:=make(chan int)ch:=make(chan int,0)ch:=make(chan int,2) |
看例子,其中第一個和第二個初始化是等價的。第三個初始化建立了一個大小為2的通道,這種稱為有緩衝通道。
無緩衝的通道
無緩衝的通道指的是通道的大小為0,也就是說,這種類型的通道在接收前沒有能力儲存任何值,它要求發送goroutine和接收goroutine同時準備好,才可以完成發送和接收操作。
從上面無緩衝的通道定義來看,發送goroutine和接收gouroutine必須是同步的,同時準備後,如果沒有同時準備好的話,先執行的操作就會阻塞等待,直到另一個相對應的操作準備好為止。這種無緩衝的通道我們也稱之為同步通道。
1234567891011121314 |
func main() {ch := make(chan int)go func() {var sum int = 0for 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的時候,有個管道操作|,它的意思是把上一個操作的輸出,當成下一個操作的輸入,連起來,做一連串的處理操作。
1234 |
➜ ~ ls |grep 'D' DesktopDocumentsDownloads |
比如上面這個例子的意思是,先使用ls命令,把目前的目錄下的目錄和檔案列出來,作為下一個grep命令的輸入,然後通過grep命令,匹配我們需要顯示的目錄和檔案,這裡匹配以D開頭的檔案名稱或者目錄名。
其實我們使用通道也可以做到管道的效果,我們只需要把一個通道的輸出,當成下一個通道的輸入即可。
12345678910111213141516 |
func main() {one := make(chan int)two := make(chan int)go func() {one<-100}()go func() {v:=<-onetwo<-v}()fmt.Println(<-two)} |
這裡例子中我們定義兩個通道one和two,然後按照順序,先把100發送給通道one,然後用另外一個goroutine從one接收值,再發送給通道two,最終在主goroutine裡等著接收列印two通道裡的值,這就類似於一個管道的操作,把通道one的輸出,當成通道two的輸入,類似於接力賽一樣。
有緩衝的通道
有緩衝通道,其實是一個隊列,這個隊列的最大容量就是我們使用make函數建立通道時,通過第二個參數指定的。
1 |
ch := make(chan int, 3) |
這裡建立容量為3的,有緩衝的通道。對於有緩衝的通道,向其發送操作就是向隊列的尾部插入元素,接收操作則是從隊列的頭部刪除元素,並返回這個剛剛刪除的元素。
當隊列滿的時候,發送操作會阻塞;當隊列空的時候,接受操作會阻塞。有緩衝的通道,不要求發送和接收操作時同步的,相反可以解耦發送和接收操作。
想知道通道的容量以及裡面有幾個元素資料怎麼辦?其實和map一樣,使用cap和len函數就可以了。
cap函數返回通道的最大容量,len函數返回現在通道裡有幾個元素。
12345678 |
func mirroredQuery() 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}func request(hostname string) (response string) { /* ... */ } |
這是Go語言聖經裡比較有意義的一個例子,例子是想擷取服務端的一個資料,不過這個資料在三個鏡像網站上都存在,這三個鏡像分散在不同的地理位置,而我們的目的又是想最快的擷取到資料。
所以這裡,我們定義了一個容量為3的通道responses,然後同時發起3個並發goroutine向這三個鏡像擷取資料,擷取到的資料發送到通道responses中,最後我們使用return <-responses返回擷取到的第一個資料,也就是最快返回的那個鏡像的資料。
單向通道
有時候,我們有一些特殊情境,比如限制一個通道只可以接收,但是不能發送;有時候限制一個通道只能發送,但是不能接收,這種通道我們稱為單向通道。
定義單向通道也很簡單,只需要在定義的時候,帶上<-即可。
12 |
var send chan<- int //只能發送var receive <-chan int //只能接收 |
注意<-操作符的為止,在後面是只能發送,對應發送操作;在前面是只能接收,對應接收操作。
單向通道應用於函數或者方法的參數比較多,比如
12 |
func counter(out chan<- int) {} |
例子這樣的,只能進行發送操作,防止誤操作,使用了接收操作,如果使用了接收操作,在編譯的時候就會報錯的。
使用通道可以很簡單的在goroutine之間共用資料,下一篇會具體介紹一些例子,以便更好的理解並發。
《Go語言實戰》讀書筆記,未完待續,歡迎掃碼關注公眾號flysnow_org,第一時間看後續筆記。覺得有協助的話,順手分享到朋友圈吧,感謝支援。