面试题答案
一键面试死锁场景描述
在Go语言中,一个常见的由于Goroutine通信导致死锁的场景是:两个Goroutine通过无缓冲通道相互发送和接收数据,且发送和接收操作都在对方之前执行,从而导致互相等待,形成死锁。例如:
package main
import (
"fmt"
)
func main() {
ch := make(chan int)
go func() {
ch <- 1
fmt.Println("Sent 1 to channel")
}()
value := <-ch
fmt.Println("Received value:", value)
}
在这个例子中,主Goroutine试图从通道ch
接收数据,而另一个Goroutine试图向通道ch
发送数据。但是,由于主Goroutine先执行接收操作,而另一个Goroutine还未开始执行发送操作,主Goroutine就会一直阻塞等待,而另一个Goroutine由于主Goroutine没有接收,也无法发送数据,从而导致死锁。
代码分析手段
- 代码审查:仔细检查代码中所有涉及通道操作的地方,查看是否存在发送和接收操作的顺序不合理,或者是否存在没有对应的接收者的发送操作,或没有对应的发送者的接收操作。在上述例子中,通过代码审查可以发现主Goroutine和新启动的Goroutine之间通道操作顺序存在问题。
- 通道操作逻辑梳理:梳理通道的使用逻辑,明确每个通道的预期用途,即哪些Goroutine应该发送数据,哪些应该接收数据,以及发送和接收的时机。
调试手段
- 使用
go run -race
:Go语言提供了内置的竞态检测器。在运行程序时使用go run -race
命令,它能够检测到大多数竞态条件和死锁情况。例如,对于上述代码,运行go run -race main.go
会输出类似如下的死锁信息:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
/path/to/main.go:10 +0x110
created by main.main
/path/to/main.go:7 +0x70
- 添加日志输出:在Goroutine的关键位置添加日志输出,例如在发送和接收操作前后打印一些信息,以了解程序执行的顺序。修改上述代码如下:
package main
import (
"fmt"
)
func main() {
ch := make(chan int)
go func() {
fmt.Println("Before sending 1 to channel")
ch <- 1
fmt.Println("Sent 1 to channel")
}()
fmt.Println("Before receiving from channel")
value := <-ch
fmt.Println("Received value:", value)
}
通过观察日志输出,可以发现Before receiving from channel
打印后程序卡住,从而推断出接收操作在发送操作之前执行,导致死锁。
解决方法
- 调整操作顺序:在上述例子中,可以先启动Goroutine,然后再进行接收操作,确保发送操作先执行。修改后的代码如下:
package main
import (
"fmt"
)
func main() {
ch := make(chan int)
go func() {
ch <- 1
fmt.Println("Sent 1 to channel")
}()
fmt.Println("Before receiving from channel")
value := <-ch
fmt.Println("Received value:", value)
}
- 使用带缓冲通道:可以将无缓冲通道改为带缓冲通道,这样发送操作不会立即阻塞,直到缓冲区满。例如:
package main
import (
"fmt"
)
func main() {
ch := make(chan int, 1)
ch <- 1
fmt.Println("Sent 1 to channel")
value := <-ch
fmt.Println("Received value:", value)
}
这样即使接收操作稍后执行,发送操作也能正常完成。