这段摘要概括了本文的核心内容:go 语言 select 语句在使用时可能因为 busy loop 导致某些 case 分支长时间无法被执行,称为“饥饿”现象。通过一个 time.Ticker 的例子解释了原因,并提供了 runtime.Gosched() 的解决方案。同时提醒开发者在涉及 I/O 或其他调度器触发场景下谨慎评估 select 语句的行为。
理解 Go 语言 select 语句的“饥饿”现象
在使用 Go 语言编写并发程序时,select 语句是一个非常重要的工具,它允许我们同时监听多个 channel 上的操作,并在其中一个 channel 可用时执行相应的代码块。然而,如果不小心使用 select 语句,可能会遇到“饥饿”现象,即某些 case 分支长时间无法被执行。
一个典型的例子是使用 time.Ticker 来周期性地执行某些任务,并将其与 select 语句结合使用:
package main import ( "fmt" "time" "runtime" ) func main() { rt := time.NewTicker(time.Second / 60) defer rt.Stop() // 确保程序退出时停止 ticker for { select { case <-rt.C: fmt.Println("time") default: // 一些默认操作 } } }
在这个例子中,我们期望 time.Ticker 每隔 1/60 秒向 channel rt.C 发送一个值,从而触发 select 语句的第一个 case 分支。然而,如果没有其他措施,程序很可能会陷入一个 busy loop,不断地执行 default 分支,而 <-rt.C 永远没有机会被执行。
立即学习“go语言免费学习笔记(深入)”;
为什么会出现“饥饿”现象?
出现这种现象的原因在于 Go 语言的调度器是协作式的。这意味着 Goroutine 只有在主动放弃 CPU 时间片时,其他 Goroutine 才能获得运行的机会。在上面的例子中,for 循环一直在运行,并且 select 语句的 default 分支也在快速执行。如果 default 分支没有进行任何 I/O 操作或者其他可以触发调度器的操作,那么 time.Ticker 所在的 Goroutine 就没有机会运行,也就无法向 rt.C 发送数据。
换句话说,select 语句陷入了 busy loop,它一直在检查 rt.C 是否有数据,但由于 time.Ticker 没有机会运行,rt.C 永远是空的。
如何解决“饥饿”现象?
解决“饥饿”现象的关键在于让 select 语句能够主动让出 CPU 时间片,给其他 Goroutine 运行的机会。一种简单有效的方法是使用 runtime.Gosched() 函数:
package main import ( "fmt" "time" "runtime" ) func main() { rt := time.NewTicker(time.Second / 60) defer rt.Stop() for { select { case <-rt.C: fmt.Println("time") default: runtime.Gosched() // 主动让出 CPU 时间片 // 一些默认操作 } } }
runtime.Gosched() 函数的作用是让当前 Goroutine 放弃 CPU 时间片,让调度器重新调度其他 Goroutine。这样,time.Ticker 所在的 Goroutine 就可以获得运行的机会,从而向 rt.C 发送数据,使得 select 语句的第一个 case 分支能够被执行。
另一种方法是使用 time.Sleep() 函数,让当前 Goroutine 休眠一段时间:
package main import ( "fmt" "time" ) func main() { rt := time.NewTicker(time.Second / 60) defer rt.Stop() for { select { case <-rt.C: fmt.Println("time") default: time.Sleep(time.Millisecond) // 休眠 1 毫秒 // 一些默认操作 } } }
time.Sleep() 函数会让当前 Goroutine 休眠指定的时间,从而让其他 Goroutine 获得运行的机会。
注意事项
虽然 runtime.Gosched() 和 time.Sleep() 都可以解决“饥饿”现象,但它们的使用需要谨慎。过度使用 runtime.Gosched() 会导致频繁的 Goroutine 切换,降低程序的性能。而 time.Sleep() 则可能会引入不必要的延迟。
在实际开发中,应该根据具体情况选择合适的解决方案。如果 default 分支需要执行一些耗时的操作,可以考虑使用 runtime.Gosched() 让出 CPU 时间片。如果 default 分支的操作比较简单,可以考虑使用 time.Sleep() 休眠一段时间。
此外,还需要注意 select 语句中各个 case 分支的优先级。如果某些 case 分支的条件总是满足,那么其他 case 分支可能会一直无法被执行。在这种情况下,需要重新设计 select 语句的逻辑,避免出现优先级不平衡的问题。
总结
select 语句是 Go 语言中一个强大的并发工具,但如果不小心使用,可能会遇到“饥饿”现象。理解“饥饿”现象的原因,并掌握相应的解决方案,可以帮助我们编写出更加健壮和高效的并发程序。在使用 select 语句时,需要注意以下几点:
- 避免 busy loop,让 select 语句能够主动让出 CPU 时间片。
- 根据具体情况选择合适的解决方案,如 runtime.Gosched() 或 time.Sleep()。
- 注意 select 语句中各个 case 分支的优先级,避免出现优先级不平衡的问题。