Go select 死鎖引發的思考
阿新 • • 發佈:2022-03-07
Go select 死鎖引發的思考
上文總結
總結一
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() 都會按照原始碼順序執行:getVal(1) 和 getVal(2),也就是它們必然先輸出:
getVal, i= 1
getVal, i= 2
總結二
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) } }
每次進入以下 select 語句時:
select {
case ch <- <-input1:
case ch <- <-input2:
}
<-input1
和 <-input2
都會執行,相應的值是:A x 和 B x(其中 x 是 0-5)。但每次 select 只會選擇其中一個 case 執行,所以 <-input1
和 <-input2
的結果,必然有一個被丟棄了,也就是不會被寫入 ch 中。因此,一共只會輸出 5 次,另外 5 次結果丟掉了。(你會發現,輸出的 5 次結果中,x 比如是 0 1 2 3 4)
而 main 中迴圈 10 次,只獲得 5 次結果,所以輸出 5 次後,報死鎖。
如果改為這樣就一切正常:
select {
case t := <-input1:
ch <- t
case t := <-input2:
ch <- t
}
我的理解:
case ch <- <-input:
語句是分成兩段執行的,可以理解為
t := <- input //case選擇還未明確的時候會執行
ch <- t //如果沒有選擇此case,則不執行此語句
並且這是兩條語句,具有先後順序
所以<-input 執行後,沒有選擇此case,<-input的結果就會被丟棄掉,從而導致上述的死鎖問題。
問題的引申
上述提到
無論 select 最終選擇了哪個 case,getVal() 都會按照原始碼順序執行:getVal(1) 和 getVal(2),也就是它們必然先輸出:
getVal, i= 1
getVal, i= 2
思考一:如果getVal()方法執行的時間不同,select的執行時長是取決於執行時間長的,還是時間的總和?
func getVal1(i int) int {
time.Sleep(time.Second * 1)
fmt.Println("getVal, i=", i)
return i
}
func getVal2(i int) int {
time.Sleep(time.Second * 2)
fmt.Println("getVal, i=", i)
return i
}
func main() {
ch := make(chan int)
go func() {
for {
beginTime := time.Now()
select {
case ch <- getVal1(1):
case ch <- getVal2(2):
default:
fmt.Println("")
}
fmt.Println(time.Since(beginTime))
}
}()
time.Sleep(time.Second * 10)
}
輸出的結果
getVal, i= 1
getVal, i= 2
3.0015862s
getVal, i= 1
getVal, i= 2
3.0021938s
getVal, i= 1
getVal, i= 2
3.0019246s
可以看出來,每次select都會按順序執行case語句,並且select的執行時間為case語句的總和
當然在實際生產中也不會有這種寫法
正確的寫法:
func main() {
begin := time.Now()
ch := make(chan int)
ch2 := make(chan int, 2)
go func() {
ch2 <- getVal1(1)
}()
go func() {
ch2 <- getVal2(2)
}()
go func() {
for {
select {
case d := <-ch2:
ch <- d
}
}
}()
for i := 0; i < 2; i++ {
fmt.Println(<-ch)
}
fmt.Println(time.Since(begin))
}
輸出結果,此時取決於執行時間最長的getVal()
getVal, i= 1
1
getVal, i= 2
2
2.0020979s
在實際生產中,select語句只用於接受channel中的數值,而不是去執行某一方法
細心的小夥伴已經發現了,上述的寫法有兩個bug
- 新起協程中,因為for語句導致一直空轉,該協程不會被銷燬
- 如果ch被close以後,對其傳送資料,會導致panic
加點註釋看看輸出的結果
func main() {
begin := time.Now()
ch := make(chan int)
ch2 := make(chan int, 2)
go func() {
ch2 <- getVal1(1)
}()
go func() {
ch2 <- getVal2(2)
}()
time.Sleep(2 * time.Second)
fmt.Println("goroutine num", runtime.NumGoroutine())
go func() {
defer func() {
if r := recover(); r != nil {
fmt.Println("panic err", r)
}
}()
for {
select {
case d := <-ch2:
ch <- d
}
}
}()
for i := 0; i < 2; i++ {
fmt.Println(<-ch)
}
close(ch)
fmt.Println(time.Since(begin))
fmt.Println("goroutine num", runtime.NumGoroutine())
ch2 <- 1
time.Sleep(time.Second * 1)
}
輸出的結果
getVal, i= 1
getVal, i= 2
goroutine num 2
1
2
2.0020965s
goroutine num 2
panic err send on closed channel
可以看到,for迴圈的協程並沒有被釋放,並且在後續的ch <-
操作中也報出了panic異常