Go select 竟然死鎖了。。。
大家好,我是 polarisxu。
前兩天,火丁筆記發(fā)了一篇文章:《一個 select 死鎖問題》[1],又是一個小細節(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 應該執(zhí)行 default 分支,程序正常運行。但結(jié)果卻不是,而是死鎖??梢酝ㄟ^該鏈接測試: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.
不知道大家看懂沒有?于是,最后來了一個例子驗證你是否理解了:為什么每次都是輸出一半數(shù)據(jù),然后死鎖?(同樣,這里可以運行查看結(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 上的一個問題:https://stackoverflow.com/questions/51167940/chained-channel-operations-in-a-single-select-case。
關鍵點和文章開頭例子一樣,在于 select case 中兩個 channel 串起來,即 fanIn 函數(shù)中:
- select {
- case ch <- <-input1:
- case ch <- <-input2:
- }
如果改為這樣就一切正常:
- select {
- case t := <-input1:
- ch <- t
- case t := <-input2:
- ch <- t
- }
結(jié)合這個更復雜的例子分析 Go 語言規(guī)范中的那句話。
對于 select 語句,在進入該語句時,會按源碼的順序?qū)γ恳粋€ case 子句進行求值:這個求值只針對發(fā)送或接收操作的額外表達式。
比如:
- // ch 是一個 chan int;
- // getVal() 返回 int
- // input 是 chan int
- // getch() 返回 chan int
- select {
- case ch <- getVal():
- case ch <- <-input:
- case getch() <- 1:
- case <- getch():
- }
在沒有選擇某個具體 case 執(zhí)行前,例子中的 getVal()、<-input 和 getch() 會執(zhí)行。這里有一個驗證的例子: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 最終選擇了哪個 case,getVal() 都會按照源碼順序執(zhí)行:getVal(1) 和 getVal(2),也就是它們必然先輸出:
- getVal, i= 1
- getVal, i= 2
你可以仔細琢磨一下。
現(xiàn)在回到 StackOverflow 上的那個問題。
每次進入以下 select 語句時:
- select {
- case ch <- <-input1:
- case ch <- <-input2:
- }
<-input1 和 <-input2 都會執(zhí)行,相應的值是:A x 和 B x(其中 x 是 0-5)。但每次 select 只會選擇其中一個 case 執(zhí)行,所以 <-input1 和 <-input2 的結(jié)果,必然有一個被丟棄了,也就是不會被寫入 ch 中。因此,一共只會輸出 5 次,另外 5 次結(jié)果丟掉了。(你會發(fā)現(xiàn),輸出的 5 次結(jié)果中,x 比如是 0 1 2 3 4)
而 main 中循環(huán) 10 次,只獲得 5 次結(jié)果,所以輸出 5 次后,報死鎖。
雖然這是一個小細節(jié),但實際開發(fā)中還是有可能出現(xiàn)的。比如文章提到的例子寫法:
- // ch 是一個 chan int;
- // getVal() 返回 int
- // input 是 chan int
- // getch() 返回 chan int
- select {
- case ch <- getVal():
- case ch <- <-input:
- case getch() <- 1:
- case <- getch():
- }
因此在使用 select 時,一定要注意這種可能的問題。
不要以為這個問題不會遇到,其實很常見。最多的就是 time.After 導致內(nèi)存泄露問題,網(wǎng)上有很多文章解釋原因,如何避免,其實最根本原因就是因為 select 這個機制導致的。
比如如下代碼,有內(nèi)存泄露(傳遞給 time.After 的時間參數(shù)越大,泄露會越厲害),你能解釋原因嗎?
- 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())
- }
- }
- }
參考資料
[1]《一個 select 死鎖問題》: https://blog.huoding.com/2021/08/29/947
本文轉(zhuǎn)載自微信公眾號「polarisxu」,可以通過以下二維碼關注。轉(zhuǎn)載本文請聯(lián)系polarisxu公眾號。