文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Go select 竟然死锁了。。。

2024-12-14 01:16

关注

大家好,我是 polarisxu。

前两天,火丁笔记发了一篇文章:《一个 select 死锁问题》[1],又是一个小细节。我将其中的问题改一下,更好理解:

  1. package main 
  2.  
  3. import "sync" 
  4.  
  5. func main() { 
  6.  var wg sync.WaitGroup 
  7.  foo := make(chan int
  8.  bar := make(chan int
  9.  wg.Add(1) 
  10.  go func() { 
  11.   defer wg.Done() 
  12.   select { 
  13.   case foo <- <-bar: 
  14.   default
  15.    println("default"
  16.   } 
  17.  }() 
  18.  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)

  1. package main 
  2.  
  3. import ( 
  4.  "fmt" 
  5.  "time" 
  6.  
  7. func talk(msg string, sleep int) <-chan string { 
  8.  ch := make(chan string) 
  9.  go func() { 
  10.   for i := 0; i < 5; i++ { 
  11.    ch <- fmt.Sprintf("%s %d", msg, i) 
  12.    time.Sleep(time.Duration(sleep) * time.Millisecond) 
  13.   } 
  14.  }() 
  15.  return ch 
  16.  
  17. func fanIn(input1, input2 <-chan string) <-chan string { 
  18.  ch := make(chan string) 
  19.  go func() { 
  20.   for { 
  21.    select { 
  22.    case ch <- <-input1: 
  23.    case ch <- <-input2: 
  24.    } 
  25.   } 
  26.  }() 
  27.  return ch 
  28.  
  29. func main() { 
  30.  ch := fanIn(talk("A", 10), talk("B", 1000)) 
  31.  for i := 0; i < 10; i++ { 
  32.   fmt.Printf("%q\n", <-ch) 
  33.  } 

有没有这种感觉:

算法入门

这是 StackOverflow 上的一个问题:https://stackoverflow.com/questions/51167940/chained-channel-operations-in-a-single-select-case。

关键点和文章开头例子一样,在于 select case 中两个 channel 串起来,即 fanIn 函数中:

  1. select { 
  2. case ch <- <-input1: 
  3. case ch <- <-input2: 

如果改为这样就一切正常:

  1. select { 
  2. case t := <-input1: 
  3.   ch <- t 
  4. case t := <-input2: 
  5.   ch <- t 

结合这个更复杂的例子分析 Go 语言规范中的那句话。

对于 select 语句,在进入该语句时,会按源码的顺序对每一个 case 子句进行求值:这个求值只针对发送或接收操作的额外表达式。

比如:

  1. // ch 是一个 chan int; 
  2. // getVal() 返回 int 
  3. // input 是 chan int 
  4. // getch() 返回 chan int 
  5. select { 
  6.   case ch <- getVal(): 
  7.   case ch <- <-input: 
  8.   case getch() <- 1: 
  9.   case <- getch(): 

在没有选择某个具体 case 执行前,例子中的 getVal()、<-input 和 getch() 会执行。这里有一个验证的例子:https://play.studygolang.com/p/DkpCq3aQ1TE。

  1. package main 
  2.  
  3. import ( 
  4.  "fmt" 
  5.  
  6. func main() { 
  7.  ch := make(chan int
  8.  go func() { 
  9.   select { 
  10.   case ch <- getVal(1): 
  11.    fmt.Println("in first case"
  12.   case ch <- getVal(2): 
  13.    fmt.Println("in second case"
  14.   default
  15.    fmt.Println("default"
  16.   } 
  17.  }() 
  18.  
  19.  fmt.Println("The val:", <-ch) 
  20.  
  21. func getVal(i intint { 
  22.  fmt.Println("getVal, i=", i) 
  23.  return i 

无论 select 最终选择了哪个 case,getVal() 都会按照源码顺序执行:getVal(1) 和 getVal(2),也就是它们必然先输出:

  1. getVal, i= 1 
  2. getVal, i= 2 

你可以仔细琢磨一下。

现在回到 StackOverflow 上的那个问题。

每次进入以下 select 语句时:

  1. select { 
  2. case ch <- <-input1: 
  3. 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 次后,报死锁。

虽然这是一个小细节,但实际开发中还是有可能出现的。比如文章提到的例子写法:

  1. // ch 是一个 chan int; 
  2. // getVal() 返回 int 
  3. // input 是 chan int 
  4. // getch() 返回 chan int 
  5. select { 
  6.   case ch <- getVal(): 
  7.   case ch <- <-input: 
  8.   case getch() <- 1: 
  9.   case <- getch(): 

因此在使用 select 时,一定要注意这种可能的问题。

不要以为这个问题不会遇到,其实很常见。最多的就是 time.After 导致内存泄露问题,网上有很多文章解释原因,如何避免,其实最根本原因就是因为 select 这个机制导致的。

比如如下代码,有内存泄露(传递给 time.After 的时间参数越大,泄露会越厉害),你能解释原因吗?

  1. package main 
  2.  
  3. import ( 
  4.     "time" 
  5.  
  6. func main()  { 
  7.     ch := make(chan int, 10) 
  8.  
  9.     go func() { 
  10.         var i = 1 
  11.         for { 
  12.             i++ 
  13.             ch <- i 
  14.         } 
  15.     }() 
  16.  
  17.     for { 
  18.         select { 
  19.         case x := <- ch: 
  20.             println(x) 
  21.         case <- time.After(30 * time.Second): 
  22.             println(time.Now().Unix()) 
  23.         } 
  24.     } 

参考资料

[1]《一个 select 死锁问题》: https://blog.huoding.com/2021/08/29/947

本文转载自微信公众号「polarisxu」,可以通过以下二维码关注。转载本文请联系polarisxu公众号。

 

来源:polarisxu内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯