Go select 死鎖的一個(gè)細(xì)節(jié)
下面對(duì)是一個(gè) select
死鎖的問題
package main import "sync" func main() { var wg sync.WaitGroup foo := make(chan int) bar := make(chan int) wg.Add(1) go func() { defer wg.Done() select { case foo <- <-bar: default: println("default") } }() wg.Wait() }
按常規(guī)理解,go func
中的 select
應(yīng)該執(zhí)行 default
分支,程序正常運(yùn)行。但結(jié)果卻不是,而是死鎖??梢酝ㄟ^該鏈接測(cè)試:https://play.studygolang.com/p/kF4pOjYXbXf。
原因文章也解釋了,Go 語言規(guī)范中有這么一句:
For all the cases in the statement, the channel operands of receive operations and the channel and right-hand-side expressions of send statements are evaluated exactly once, in source order, upon entering the “select” statement. The result is a set of channels to receive from or send to, and the corresponding values to send. Any side effects in that evaluation will occur irrespective of which (if any) communication operation is selected to proceed. Expressions on the left-hand side of a RecvStmt with a short variable declaration or assignment are not yet evaluated.
不知道大家看懂沒有?于是,最后來了一個(gè)例子驗(yàn)證你是否理解了:為什么每次都是輸出一半數(shù)據(jù),然后死鎖?(同樣,這里可以運(yùn)行查看結(jié)果:https://play.studygolang.com/p/zoJtTzI7K5T)
package main import ( "fmt" "time" ) func talk(msg string, sleep int) <-chan string { ch := make(chan string) go func() { for i := 0; i < 5; i++ { ch <- fmt.Sprintf("%s %d", msg, i) time.Sleep(time.Duration(sleep) * time.Millisecond) } }() return ch } func fanIn(input1, input2 <-chan string) <-chan string { ch := make(chan string) go func() { for { select { case ch <- <-input1: case ch <- <-input2: } } }() return ch } func main() { ch := fanIn(talk("A", 10), talk("B", 1000)) for i := 0; i < 10; i++ { fmt.Printf("%q\n", <-ch) } }
有沒有這種感覺:
這是 StackOverflow 上的一個(gè)問題:https://stackoverflow.com/questions/51167940/chained-channel-operations-in-a-single-select-case。
關(guān)鍵點(diǎn)和文章開頭例子一樣,在于 select case
中兩個(gè) channel
串起來,即 fanIn
函數(shù)中:
select { case ch <- <-input1: case ch <- <-input2: }
如果改為這樣就一切正常:
select { case t := <-input1: ch <- t case t := <-input2: ch <- t }
結(jié)合這個(gè)更復(fù)雜的例子分析 Go 語言規(guī)范中的那句話。
對(duì)于 select
語句,在進(jìn)入該語句時(shí),會(huì)按源碼的順序?qū)γ恳粋€(gè) case 子句進(jìn)行求值:這個(gè)求值只針對(duì)發(fā)送或接收操作的額外表達(dá)式。
比如:
// ch 是一個(gè) chan int; // getVal() 返回 int // input 是 chan int // getch() 返回 chan int select { case ch <- getVal(): case ch <- <-input: case getch() <- 1: case <- getch(): }
在沒有選擇某個(gè)具體 case
執(zhí)行前,例子中的 getVal()
、 <-input
和 getch()
會(huì)執(zhí)行。這里有一個(gè)驗(yàn)證的例子:https://play.studygolang.com/p/DkpCq3aQ1TE。
package main import ( "fmt" ) func main() { ch := make(chan int) go func() { select { case ch <- getVal(1): fmt.Println("in first case") case ch <- getVal(2): fmt.Println("in second case") default: fmt.Println("default") } }() fmt.Println("The val:", <-ch) } func getVal(i int) int { fmt.Println("getVal, i=", i) return i }
無論 select
最終選擇了哪個(gè) case
, getVal()
都會(huì)按照源碼順序執(zhí)行: getVal(1)
和 getVal(2)
,也就是它們必然先輸出:
getVal, i= 1 getVal, i= 2
你可以仔細(xì)琢磨一下。
現(xiàn)在回到 StackOverflow
上的那個(gè)問題。
每次進(jìn)入以下 select 語句時(shí):
select { case ch <- <-input1: case ch <- <-input2: }
<-input1 和 <-input2
都會(huì)執(zhí)行,相應(yīng)的值是:A x 和 B x(其中 x 是 0-5)。但每次 select 只會(huì)選擇其中一個(gè) case 執(zhí)行,所以 <-input1 和 <-input2
的結(jié)果,必然有一個(gè)被丟棄了,也就是不會(huì)被寫入 ch 中。因此,一共只會(huì)輸出 5 次,另外 5 次結(jié)果丟掉了。(你會(huì)發(fā)現(xiàn),輸出的 5 次結(jié)果中,x 比如是 0 1 2 3 4)
而 main
中循環(huán) 10 次,只獲得 5 次結(jié)果,所以輸出 5 次后,報(bào)死鎖。
雖然這是一個(gè)小細(xì)節(jié),但實(shí)際開發(fā)中還是有可能出現(xiàn)的。比如文章提到的例子寫法:
// ch 是一個(gè) chan int; // getVal() 返回 int // input 是 chan int // getch() 返回 chan int select { case ch <- getVal(): case ch <- <-input: case getch() <- 1: case <- getch(): }
因此在使用 select
時(shí),一定要注意這種可能的問題。
不要以為這個(gè)問題不會(huì)遇到,其實(shí)很常見。最多的就是 time.After
導(dǎo)致內(nèi)存泄露問題,網(wǎng)上有很多文章解釋原因,如何避免,其實(shí)最根本原因就是因?yàn)?select
這個(gè)機(jī)制導(dǎo)致的。
比如如下代碼,有內(nèi)存泄露(傳遞給 time.After
的時(shí)間參數(shù)越大,泄露會(huì)越厲害),你能解釋原因嗎?
package main import ( "time" ) func main() { ch := make(chan int, 10) go func() { var i = 1 for { i++ ch <- i } }() for { select { case x := <- ch: println(x) case <- time.After(30 * time.Second): println(time.Now().Unix()) } } }
到此這篇關(guān)于Go select 死鎖的一個(gè)細(xì)節(jié)的文章就介紹到這了,更多相關(guān)Go select
死鎖內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
golang基于errgroup實(shí)現(xiàn)并發(fā)調(diào)用的方法
這篇文章主要介紹了golang基于errgroup實(shí)現(xiàn)并發(fā)調(diào)用,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-09-09Go語言實(shí)現(xiàn)MapReduce的示例代碼
MapReduce是一種備受歡迎的編程模型,它最初由Google開發(fā),用于并行處理大規(guī)模數(shù)據(jù)以提取有價(jià)值的信息,本文將使用GO語言實(shí)現(xiàn)一個(gè)簡(jiǎn)單的MapReduce,需要的可以參考下2023-10-10一文詳解go中如何實(shí)現(xiàn)定時(shí)任務(wù)
定時(shí)任務(wù)是指按照預(yù)定的時(shí)間間隔或特定時(shí)間點(diǎn)自動(dòng)執(zhí)行的計(jì)劃任務(wù)或操作,這篇文章主要為大家詳細(xì)介紹了go中是如何實(shí)現(xiàn)定時(shí)任務(wù)的,感興趣的可以了解下2023-11-11Go存儲(chǔ)基礎(chǔ)使用direct io方法實(shí)例
這篇文章主要介紹了Go存儲(chǔ)基礎(chǔ)之如何使用direct io方法實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12GoFrame框架Scan類型轉(zhuǎn)換實(shí)例
這篇文章主要為大家介紹了GoFrame框架Scan類型轉(zhuǎn)換的實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06Go簡(jiǎn)單實(shí)現(xiàn)協(xié)程池的實(shí)現(xiàn)示例
本文主要介紹了Go簡(jiǎn)單實(shí)現(xiàn)協(xié)程池的實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06Go語言實(shí)現(xiàn)的web爬蟲實(shí)例
這篇文章主要介紹了Go語言實(shí)現(xiàn)的web爬蟲,實(shí)例分析了web爬蟲的原理與Go語言的實(shí)現(xiàn)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-02-02Go語言實(shí)現(xiàn)冒泡排序、選擇排序、快速排序及插入排序的方法
這篇文章主要介紹了Go語言實(shí)現(xiàn)冒泡排序、選擇排序、快速排序及插入排序的方法,以實(shí)例形式詳細(xì)分析了幾種常見的排序技巧與實(shí)現(xiàn)方法,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-02-02