1 為什么會有信道
協(xié)程(goroutine)算是Go的一大新特性麻削,也正是這個大殺器讓Go為很多路人駐足欣賞猪瞬,讓信徒們?yōu)橹畾g呼津津樂道。
協(xié)程的使用也很簡單顿锰,在Go中使用關鍵字“go“后面跟上要執(zhí)行的函數(shù)即表示新啟動一個協(xié)程中執(zhí)行功能代碼。
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中的協(xié)程就是一種更輕硼控、支持更高并發(fā)的并發(fā)機制。
仔細看上面的main函數(shù)中有一個休眠一秒的操作胳赌,如果去掉該行牢撼,則打印結果中就沒有“it is a new goroutine”。這是因為新啟的協(xié)程還沒來得及運行疑苫,主協(xié)程就結束了熏版。
所以這里有個問題,我們怎么樣才能讓各個協(xié)程之間能夠知道彼此是否執(zhí)行完畢呢捍掺?
顯然撼短,我們可以通過上面的方式,讓主協(xié)程休眠一秒鐘乡小,等等子協(xié)程阔加,確保子協(xié)程能夠執(zhí)行完。但作為一個新型語言不應該使用這么low的方式啊满钟。連Java這位老前輩都有Future這種異步機制胜榔,而且可以通過get方法來阻塞等待任務的執(zhí)行,確迸确可以第一時間知曉異步進程的執(zhí)行狀態(tài)夭织。
所以,Go必須要有過人之處吠撮,即另一個讓路人側目尊惰,讓信徒為之瘋狂的特性——信道(channel)。
2 信道如何使用
信道可以簡單認為是協(xié)程goroutine之間一個通信的橋梁,可以在不同的協(xié)程里互通有無穿梭自如弄屡,且是線程安全的题禀。
2.1 信道分類
信道分為兩類
無緩沖信道
ch := make(chan string)
有緩沖信道
ch := make(chan string, 2)
2.2 兩類信道的區(qū)別
1、從聲明方式來看膀捷,有緩沖帶了容量迈嘹,即后面的數(shù)字,這里的2表示信道可以存放兩個stirng類型的變量
2全庸、無緩沖信道本身不存儲信息秀仲,它只負責轉手,有人傳給它壶笼,它就必須要傳給別人神僵,如果只有進或者只有出的操作,都會造成阻塞覆劈。有緩沖的可以存儲指定容量個變量保礼,但是超過這個容量再取值也會阻塞。
2.3 兩種信道使用舉例
無緩沖信道
func main() {
ch := make(chan string)
go func() {
ch <- "send"
}()
fmt.Println(<-ch)
}
在主協(xié)程中新啟一個協(xié)程且是匿名函數(shù)责语,在子協(xié)程中向通道發(fā)送“send”氓英,通過打印結果,我們知道在主線程使用<-ch接收到了傳給ch的值鹦筹。
<-ch是一種簡寫方式,也可以使用str := <-ch方式接收信道值址貌。
上面是在子協(xié)程中向信道傳值铐拐,并在主協(xié)程取值,也可以反過來练对,同樣可以正常打印信道的值遍蟋。
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)
}
執(zhí)行結果為
first
second
信道本身結構是一個先進先出的隊列,所以這里輸出的順序如結果所示螟凭。
從代碼來看這里也不需要重新啟動一個goroutine虚青,也不會發(fā)生死鎖(后面會講原因)。
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 死鎖現(xiàn)場1
func main() {
ch := make(chan string)
ch <- "channelValue"
}
func main() {
ch := make(chan string)
<-ch
}
這兩種情況欲主,即無論是向無緩沖信道傳值還是取值邓厕,都會發(fā)生死鎖。
原因分析
如上場景是在只有一個goroutine即主goroutine的扁瓢,且使用的是無緩沖信道的情況下详恼。
前面提過,無緩沖信道不存儲值引几,無論是傳值還是取值都會阻塞昧互。這里只有一個主協(xié)程的情況下,第一段代碼是阻塞在傳值伟桅,第二段代碼是阻塞在取值敞掘。因為一直卡住主協(xié)程,系統(tǒng)一直在等待楣铁,所以系統(tǒng)判斷為死鎖玖雁,最終報deadlock錯誤并結束程序。
延伸
func main() {
ch := make(chan string)
go func() {
ch <- "send"
}()
}
這種情況不會發(fā)生死鎖盖腕。
有人說那是因為主協(xié)程發(fā)車太快赫冬,子協(xié)程還沒看到,車就開走了溃列,所以沒來得及抱怨(deadlock)就結束了劲厌。
其實不是這樣的,下面舉個反例
func main() {
ch := make(chan string)
go func() {
ch <- "send"
}()
time.Sleep(time.Second * 3)
}
這次主協(xié)程等你了三秒听隐,三秒你總該完事了吧补鼻?!
但是從執(zhí)行結果來看雅任,并沒有子協(xié)程因為一直阻塞就造成報死鎖錯誤风范。
這是因為雖然子協(xié)程一直阻塞在傳值語句,但這也只是子協(xié)程的事沪么。外面的主協(xié)程還是該干嘛干嘛乌企,等你三秒之后就發(fā)車走人了。因為主協(xié)程都結束了成玫,所以子協(xié)程也只好結束(畢竟沒搭上車只能回家了加酵,光杵在哪也于事無補)
4.2 死鎖現(xiàn)場2
緊接著上面死鎖現(xiàn)場1的延伸場景拳喻,我們提到延伸場景沒有死鎖是因為主協(xié)程發(fā)車走了,所以子協(xié)程也只能回家猪腕。也就是兩者沒有耦合的關系冗澈。
如果兩者通過信道建立了聯(lián)系還會死鎖嗎?
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
ch2 <- "ch2 value"
ch1 <- "ch1 value"
}()
<- ch1
}
執(zhí)行結果為
fatal error: all goroutines are asleep - deadlock!
沒錯陋葡,這樣就會發(fā)生死鎖亚亲。
原因分析
上面的代碼不能保證是主線程的<-ch1先執(zhí)行還是子協(xié)程的代碼先執(zhí)行。
如果主協(xié)程先執(zhí)行到<-ch1腐缤,顯然會阻塞等待有其他協(xié)程往ch1傳值捌归。終于等到子協(xié)程運行了,結果子協(xié)程運行ch2 <- "ch2 value"就阻塞了岭粤,因為是無緩沖惜索,所以必須有下家接收值才行,但是等了半天也沒有人來傳值剃浇。
所以這時候就出現(xiàn)了主協(xié)程等子協(xié)程的ch1巾兆,子協(xié)程在等ch2的接收者,ch1<-“ch1 value”語句遲遲拿不到執(zhí)行權虎囚,于是大家都在相互等待角塑,系統(tǒng)看不下去了,判定死鎖淘讥,程序結束圃伶。
相反執(zhí)行順序也是一樣。
延伸
有人會說那我改成這樣能避免死鎖嗎
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
ch2 <- "ch2 value"
ch1 <- "ch1 value"
}()
<- ch1
<- ch2
}
不行蒲列,執(zhí)行結果依然是死鎖留攒。因為這樣的順序還是改變不了主協(xié)程和子協(xié)程相互等待的情況,即死鎖的觸發(fā)條件嫉嘀。
改為下面這樣就可以正常結束
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
ch2 <- "ch2 value"
ch1 <- "ch1 value"
}()
<- ch2
<- ch1
}
借此,通過下面的例子再驗證上面死鎖現(xiàn)場1是因為主協(xié)程沒受到死鎖的影響所以不會報死鎖錯誤的問題
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
放到主協(xié)程魄揉,則會因為相互等待發(fā)生死鎖剪侮。但是這個例子里,將同樣的代碼放到一個新啟的協(xié)程中洛退,盡管兩個子協(xié)程存在阻塞死鎖的情況瓣俯,但是不會影響主協(xié)程,所以程序執(zhí)行不會報死鎖錯誤兵怯。
4.3 死鎖現(xiàn)場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是帶有緩沖的信道媒区,但是容量只有兩個驼仪,當兩個輸出完之后掸犬,可以簡單的將此時的信道等價于無緩沖的信道。
顯然對于無緩沖的信道只是單純的讀取元素是會造成阻塞的绪爸,而且是在主協(xié)程湾碎,所以和死鎖現(xiàn)場1等價,故而會死鎖奠货。
5 總結
1介褥、信道是協(xié)程之間溝通的橋梁
2、信道分為無緩沖信道和有緩沖信道
3递惋、信道使用時要注意是否構成死鎖以及各種死鎖產生的原因