【轉】Go select 竟然死鎖了。。。

 

原文:https://mp.weixin.qq.com/s?__biz=MzAxNzY0NDE3NA==&mid=2247488032&idx=1&sn=5d0c370dd992e1dd82030e5ac5600244&chksm=9be33dc1ac94b4d7798bb7ed230c899088a5c972c1c099e48779505f89539f0a840520cd0d4d&scene=178&cur_album_id=1877309550266007553#rd

--------------------

 

大家好,我是 polarisxu。

前兩天,火丁筆記發了一篇文章:《一個 select 死鎖問題》[1],又是一個小細節。我將其中的問題改一下,更好理解:

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()
}

按常規理解,go func 中的 select 應該執行 default 分支,程序正常運行。但結果卻不是,而是死鎖。可以通過該鏈接測試:https://play.studygolang.com/p/kF4pOjYXbXf。

原因文章也解釋了,Go 語言規範中有這麼一句:

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.

不知道大家看懂沒有?於是,最後來了一個例子驗證你是否理解了:爲什麼每次都是輸出一半數據,然後死鎖?(同樣,這裏可以運行查看結果: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 函數中:

select {
case ch <- <-input1:
case ch <- <-input2:
}

如果改爲這樣就一切正常:

select {
case t := <-input1:
  ch <- t
case t := <-input2:
  ch <- t
}

結合這個更復雜的例子分析 Go 語言規範中的那句話。

對於 select 語句,在進入該語句時,會按源碼的順序對每一個 case 子句進行求值:這個求值只針對發送或接收操作的額外表達式。

比如:

// ch 是一個 chan int;
// getVal() 返回 int
// input 是 chan int
// getch() 返回 chan int
select {
  case ch <- getVal():
  case ch <- <-input:
  case getch() <- 1:
  case <- getch():
}

在沒有選擇某個具體 case 執行前,例子中的 getVal()<-input 和 getch() 會執行。這裏有一個驗證的例子: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() 都會按照源碼順序執行:getVal(1) 和 getVal(2),也就是它們必然先輸出:

getVal, i= 1
getVal, i= 2

你可以仔細琢磨一下。

現在回到 StackOverflow 上的那個問題。

每次進入以下 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 次後,報死鎖。


雖然這是一個小細節,但實際開發中還是有可能出現的。比如文章提到的例子寫法:

// 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 導致內存泄露問題,網上有很多文章解釋原因,如何避免,其實最根本原因就是因爲 select 這個機制導致的。

比如如下代碼,有內存泄露(傳遞給 time.After 的時間參數越大,泄露會越厲害),你能解釋原因嗎?

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章