Go語言學習——channel的死鎖其實沒那么復雜

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递惋、信道使用時要注意是否構成死鎖以及各種死鎖產生的原因

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末柔滔,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子萍虽,更是在濱河造成了極大的恐慌睛廊,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件贩挣,死亡現(xiàn)場離奇詭異喉前,居然都是意外死亡,警方通過查閱死者的電腦和手機王财,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進店門卵迂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人绒净,你說我怎么就攤上這事见咒。” “怎么了挂疆?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵改览,是天一觀的道長。 經常有香客問我缤言,道長宝当,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任胆萧,我火速辦了婚禮庆揩,結果婚禮上,老公的妹妹穿的比我還像新娘跌穗。我一直安慰自己订晌,他們只是感情好,可當我...
    茶點故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布蚌吸。 她就那樣靜靜地躺著锈拨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪羹唠。 梳的紋絲不亂的頭發(fā)上奕枢,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天娄昆,我揣著相機與錄音,去河邊找鬼验辞。 笑死稿黄,一個胖子當著我的面吹牛,可吹牛的內容都是我干的跌造。 我是一名探鬼主播杆怕,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼壳贪!你這毒婦竟也來了陵珍?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤违施,失蹤者是張志新(化名)和其女友劉穎互纯,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體磕蒲,經...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡留潦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了辣往。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片兔院。...
    茶點故事閱讀 39,731評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖站削,靈堂內的尸體忽然破棺而出坊萝,到底是詐尸還是另有隱情,我是刑警寧澤许起,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布十偶,位于F島的核電站,受9級特大地震影響园细,放射性物質發(fā)生泄漏惦积。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一猛频、第九天 我趴在偏房一處隱蔽的房頂上張望狮崩。 院中可真熱鬧,春花似錦伦乔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至皿淋,卻和暖如春招刹,著一層夾襖步出監(jiān)牢的瞬間恬试,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工疯暑, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留训柴,地道東北人。 一個月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓妇拯,卻偏偏與公主長得像幻馁,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子越锈,可洞房花燭夜當晚...
    茶點故事閱讀 44,629評論 2 354

推薦閱讀更多精彩內容

  • Go是并發(fā)語言甘凭,而不是并行語言稀拐。 一、并發(fā)和并行的區(qū)別 ?并發(fā)(concurrency)是指一次處理大量事情的能力...
    學生黃哲閱讀 2,295評論 1 17
  • 本節(jié)學習 什么是信道? 如何聲明信道? 信道如何收發(fā)數(shù)據(jù)? 什么是死鎖? 什么是單向信道? 如何關閉信道? 使用 ...
    酷走天涯閱讀 538評論 0 0
  • Go語言并發(fā) Go 是并發(fā)式語言丹弱,而不是并行式語言德撬。 并發(fā)是指立即處理多個任務的能力。 Go 編程語言原生支持并發(fā)...
    kakarotto閱讀 1,890評論 0 7
  • 緩沖信道 之前看到的都是無緩沖信道躲胳,無緩沖信道的發(fā)送和接收過程是阻塞的蜓洪。我們還可以創(chuàng)建一個有緩沖(Buffer)的...
    kakarotto閱讀 368評論 0 0
  • 多線程同時執(zhí)行叫做并行 并發(fā)就是在不同線程中來回切換執(zhí)行來達到并行的效果就是并發(fā) 通過go可以在當前線程中開啟一個...
    AuglyXu閱讀 6,789評論 0 9