
本文探讨了 Go 并发编程中一个常见的“海森堡 Bug”,即在某些情况下,向标准输出打印内容会导致程序行为发生改变。通过分析一个模拟理发师问题的示例,解释了 Go 调度器的运作机制,以及如何通过更合理的方式进行非阻塞的 channel 操作,避免潜在的竞争条件。
在 Go 语言的并发编程中,Goroutine 的调度是一个核心概念。理解 Goroutine 的调度方式对于编写高效且可靠的并发程序至关重要。一个常见的困惑是,为什么在一段并发代码中添加 fmt.Println 语句后,程序的行为会发生改变,甚至从阻塞状态恢复正常?
Go 语言的调度器负责在多个 Goroutine 之间分配 CPU 时间。Goroutine 只有在进行系统调用或者阻塞的 channel 操作时,才会让出 CPU 的执行权。fmt.Println 函数会触发系统调用,这使得当前 Goroutine 有机会让出 CPU,让其他 Goroutine 获得执行机会。
在没有 fmt.Println 的情况下,如果某个 Goroutine 一直在执行计算密集型任务,而没有进行任何阻塞操作,那么它可能会一直占用 CPU,导致其他 Goroutine 无法得到执行。这可能导致程序出现“饿死”现象,即某些 Goroutine 永远无法运行。
考虑一个简化的理发师问题:
package main
import "fmt"
func customer(id int, shop chan<- int) {
// 进入理发店,如果还有座位,否则离开
// fmt.Println("取消注释这行代码,程序就能正常工作")
if len(shop) < cap(shop) {
shop <- id
}
}
func barber(shop <-chan int) {
// 为进入理发店的顾客理发
for {
fmt.Println("理发师为顾客", <-shop, "理发")
}
}
func main() {
shop := make(chan int, 5) // 五个座位
go barber(shop)
for i := 0; ; i++ {
customer(i, shop)
}
}在这个例子中,customer 函数模拟顾客进入理发店,如果理发店有空位,则将顾客 ID 发送到 shop channel 中。barber 函数模拟理发师,从 shop channel 中接收顾客 ID 并为其理发。
如果没有注释掉 customer 函数中的 fmt.Println 语句,程序可以正常运行。但是,如果注释掉该语句,barber 函数可能永远无法接收到顾客 ID,导致程序阻塞。
这是因为 customer 函数在没有 fmt.Println 的情况下,可能会一直快速地向 shop channel 发送顾客 ID,而 barber 函数没有机会从 shop channel 中接收顾客 ID。当 shop channel 满时,customer 函数会阻塞,但由于 barber 函数没有机会运行,因此 shop channel 永远无法被清空,导致程序死锁。
在 customer 函数中使用 len(shop) < cap(shop) 判断 channel 是否已满存在潜在的竞争条件。在判断之后,channel 可能已经被其他 Goroutine 填满。为了更安全地进行非阻塞的 channel 发送,可以使用 select 语句:
func customer(id int, shop chan<- int) {
// 进入理发店,如果还有座位,否则离开
select {
case shop <- id:
default:
}
}select 语句会尝试向 shop channel 发送顾客 ID。如果 shop channel 已满,则会执行 default 分支,而不会阻塞。这种方式更加安全,可以避免潜在的竞争条件。
理解 Go 语言的 Goroutine 调度机制对于编写健壮的并发程序至关重要。fmt.Println 语句看似无害,但实际上会影响 Goroutine 的调度。在编写并发程序时,需要注意避免长时间占用 CPU 的计算密集型任务,并使用更安全的方式进行非阻塞的 channel 操作,以避免潜在的竞争条件和死锁。
以上就是Go 并发编程中的 Goroutine 调度与阻塞问题的详细内容,更多请关注php中文网其它相关文章!
编程怎么学习?编程怎么入门?编程在哪学?编程怎么学才快?不用担心,这里为大家提供了编程速学教程(入门课程),有需要的小伙伴保存下载就能学习啦!
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号