關(guān)于 Go select 死鎖的一個(gè)細(xì)節(jié)
前些天,火丁筆記發(fā)了一篇文章:《一個(gè) select 死鎖問題》[1],又是一個(gè)小細(xì)節(jié)。我將其中的問題改一下,更好理解:
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())
????????}
????}
}
參考資料
《一個(gè) select 死鎖問題》:?https://blog.huoding.com/2021/08/29/947
? ?

???
