1. 程式人生 > 其它 >Go select 死鎖引發的思考

Go select 死鎖引發的思考

Go select 死鎖引發的思考

https://mp.weixin.qq.com/s/Ov1FvLsLfSaY8GNzfjfMbg一文引發的延續思考

上文總結

總結一

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

  1. 新起協程中,因為for語句導致一直空轉,該協程不會被銷燬
  2. 如果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異常