1 為什么會有信道
協程(goroutine)算是Go的一大新特性,也正是這個大殺器讓Go為很多路人駐足欣賞,讓信徒們為之歡呼津津樂道。
協程的使用也很簡單,在Go中使用關鍵字“go“后面跟上要執行的函數即表示新啟動一個協程中執行功能代碼。
func main() { go test() fmt.Println("it is the main goroutine") time.Sleep(time.Second * 1) } func test() { fmt.Println("it is a new goroutine") }
可以簡單理解為,Go中的協程就是一種更輕、支持更高並發的並發機制。
仔細看上面的main函數中有一個休眠一秒的操作,如果去掉該行,則打印結果中就沒有“it is a new goroutine”。這是因為新啟的協程還沒來得及運行,主協程就結束了。
所以這里有個問題,我們怎么樣才能讓各個協程之間能夠知道彼此是否執行完畢呢?
顯然,我們可以通過上面的方式,讓主協程休眠一秒鍾,等等子協程,確保子協程能夠執行完。但作為一個新型語言不應該使用這么low的方式啊。連Java這位老前輩都有Future這種異步機制,而且可以通過get方法來阻塞等待任務的執行,確保可以第一時間知曉異步進程的執行狀態。
所以,Go必須要有過人之處,即另一個讓路人側目,讓信徒為之瘋狂的特性——信道(channel)。
2 信道如何使用
信道可以簡單認為是協程goroutine之間一個通信的橋梁,可以在不同的協程里互通有無穿梭自如,且是線程安全的。
2.1 信道分類
信道分為兩類
無緩沖信道
ch := make(chan string)
有緩沖信道
ch := make(chan string, 2)
2.2 兩類信道的區別
1、從聲明方式來看,有緩沖帶了容量,即后面的數字,這里的2表示信道可以存放兩個stirng類型的變量
2、無緩沖信道本身不存儲信息,它只負責轉手,有人傳給它,它就必須要傳給別人,如果只有進或者只有出的操作,都會造成阻塞。有緩沖的可以存儲指定容量個變量,但是超過這個容量再取值也會阻塞。
2.3 兩種信道使用舉例
無緩沖信道
func main() { ch := make(chan string) go func() { ch <- "send" }() fmt.Println(<-ch) }
在主協程中新啟一個協程且是匿名函數,在子協程中向通道發送“send”,通過打印結果,我們知道在主線程使用<-ch接收到了傳給ch的值。
<-ch是一種簡寫方式,也可以使用str := <-ch方式接收信道值。
上面是在子協程中向信道傳值,並在主協程取值,也可以反過來,同樣可以正常打印信道的值。
func main() { ch := make(chan string) go func() { fmt.Println(<-ch) }() ch <- "send" }
有緩沖信道
func main() { ch := make(chan string, 2) ch <- "first" ch <- "second" fmt.Println(<-ch) fmt.Println(<-ch) }
執行結果為
first second
信道本身結構是一個先進先出的隊列,所以這里輸出的順序如結果所示。
從代碼來看這里也不需要重新啟動一個goroutine,也不會發生死鎖(后面會講原因)。
3 信道的關閉和遍歷
3.1 關閉
信道是可以關閉的。對於無緩沖和有緩沖信道關閉的語法都是一樣的。
close(channelName)
注意信道關閉了,就不能往信道傳值了,否則會報錯。
func main() { ch := make(chan string, 2) ch <- "first" ch <- "second" close(ch) ch <- "third" }
報錯信息
panic: send on closed channel
3.2 遍歷
有緩沖信道是有容量的,所以是可以遍歷的,並且支持使用我們熟悉的range遍歷。
func main() { chs := make(chan string, 2) chs <- "first" chs <- "second" for ch := range chs { fmt.Println(ch) } }
輸出結果為
first second fatal error: all goroutines are asleep - deadlock!
沒錯,如果取完了信道存儲的信息再去取信息,也會死鎖(后面會講)
4 信道死鎖
有了前面的介紹,我們大概知道了信道是什么,如何使用信道。
下面就來說說信道死鎖的場景和為什么會死鎖(有些是自己的理解,可能有偏差,如有問題請指正)。
4.1 死鎖現場1
func main() { ch := make(chan string) ch <- "channelValue" }
func main() { ch := make(chan string) <-ch }
這兩種情況,即無論是向無緩沖信道傳值還是取值,都會發生死鎖。
原因分析
如上場景是在只有一個goroutine即主goroutine的,且使用的是無緩沖信道的情況下。
前面提過,無緩沖信道不存儲值,無論是傳值還是取值都會阻塞。這里只有一個主協程的情況下,第一段代碼是阻塞在傳值,第二段代碼是阻塞在取值。因為一直卡住主協程,系統一直在等待,所以系統判斷為死鎖,最終報deadlock錯誤並結束程序。
延伸
func main() { ch := make(chan string) go func() { ch <- "send" }() }
這種情況不會發生死鎖。
有人說那是因為主協程發車太快,子協程還沒看到,車就開走了,所以沒來得及抱怨(deadlock)就結束了。
其實不是這樣的,下面舉個反例
func main() { ch := make(chan string) go func() { ch <- "send" }() time.Sleep(time.Second * 3) }
這次主協程等你了三秒,三秒你總該完事了吧?!
但是從執行結果來看,並沒有子協程因為一直阻塞就造成報死鎖錯誤。
這是因為雖然子協程一直阻塞在傳值語句,但這也只是子協程的事。外面的主協程還是該干嘛干嘛,等你三秒之后就發車走人了。因為主協程都結束了,所以子協程也只好結束(畢竟沒搭上車只能回家了,光杵在哪也於事無補)
4.2 死鎖現場2
緊接着上面死鎖現場1的延伸場景,我們提到延伸場景沒有死鎖是因為主協程發車走了,所以子協程也只能回家。也就是兩者沒有耦合的關系。
如果兩者通過信道建立了聯系還會死鎖嗎?
func main() { ch1 := make(chan string) ch2 := make(chan string) go func() { ch2 <- "ch2 value" ch1 <- "ch1 value" }() <- ch1 }
執行結果為
fatal error: all goroutines are asleep - deadlock!
沒錯,這樣就會發生死鎖。
原因分析
上面的代碼不能保證是主線程的<-ch1先執行還是子協程的代碼先執行。
如果主協程先執行到<-ch1,顯然會阻塞等待有其他協程往ch1傳值。終於等到子協程運行了,結果子協程運行ch2 <- "ch2 value"就阻塞了,因為是無緩沖,所以必須有下家接收值才行,但是等了半天也沒有人來傳值。
所以這時候就出現了主協程等子協程的ch1,子協程在等ch2的接收者,ch1<-“ch1 value”語句遲遲拿不到執行權,於是大家都在相互等待,系統看不下去了,判定死鎖,程序結束。
相反執行順序也是一樣。
延伸
有人會說那我改成這樣能避免死鎖嗎
func main() { ch1 := make(chan string) ch2 := make(chan string) go func() { ch2 <- "ch2 value" ch1 <- "ch1 value" }() <- ch1 <- ch2 }
不行,執行結果依然是死鎖。因為這樣的順序還是改變不了主協程和子協程相互等待的情況,即死鎖的觸發條件。
改為下面這樣就可以正常結束
func main() { ch1 := make(chan string) ch2 := make(chan string) go func() { ch2 <- "ch2 value" ch1 <- "ch1 value" }() <- ch2 <- ch1 }
借此,通過下面的例子再驗證上面死鎖現場1是因為主協程沒受到死鎖的影響所以不會報死鎖錯誤的問題
func main() { ch1 := make(chan string) ch2 := make(chan string) go func() { ch2 <- "ch2 value" ch1 <- "ch1 value" }() go func() { <- ch1 <- ch2 }() time.Sleep(time.Second * 2) }
我們剛剛看到如果
<- ch1 <- ch2
放到主協程,則會因為相互等待發生死鎖。但是這個例子里,將同樣的代碼放到一個新啟的協程中,盡管兩個子協程存在阻塞死鎖的情況,但是不會影響主協程,所以程序執行不會報死鎖錯誤。
4.3 死鎖現場3
func main() { chs := make(chan string, 2) chs <- "first" chs <- "second" for ch := range chs { fmt.Println(ch) } }
輸出結果為
first second fatal error: all goroutines are asleep - deadlock!
原因分析
為什么會在輸出完chs信道所有緩存值后會死鎖呢?
其實也很簡單,雖然這里的chs是帶有緩沖的信道,但是容量只有兩個,當兩個輸出完之后,可以簡單的將此時的信道等價於無緩沖的信道。
顯然對於無緩沖的信道只是單純的讀取元素是會造成阻塞的,而且是在主協程,所以和死鎖現場1等價,故而會死鎖。
5 總結
1、信道是協程之間溝通的橋梁
2、信道分為無緩沖信道和有緩沖信道
3、信道使用時要注意是否構成死鎖以及各種死鎖產生的原因
如果您覺得閱讀本文對您有幫助,請點一下“推薦”按鈕,您的“推薦”將是我最大的寫作動力!如果您想持續關注我的文章,請掃描二維碼,關注JackieZheng的微信公眾號,我會將我的文章推送給您,並和您一起分享我日常閱讀過的優質文章。