MST

星途 面试题库

面试题:Go语言Goroutine死锁场景分析与解决

请描述一种在Go语言中由于Goroutine通信导致死锁,进而表现为Goroutine卡住的场景,并详细阐述如何通过代码分析及调试手段定位并解决该问题。
19.4万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

死锁场景描述

在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没有接收,也无法发送数据,从而导致死锁。

代码分析手段

  1. 代码审查:仔细检查代码中所有涉及通道操作的地方,查看是否存在发送和接收操作的顺序不合理,或者是否存在没有对应的接收者的发送操作,或没有对应的发送者的接收操作。在上述例子中,通过代码审查可以发现主Goroutine和新启动的Goroutine之间通道操作顺序存在问题。
  2. 通道操作逻辑梳理:梳理通道的使用逻辑,明确每个通道的预期用途,即哪些Goroutine应该发送数据,哪些应该接收数据,以及发送和接收的时机。

调试手段

  1. 使用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
  1. 添加日志输出:在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打印后程序卡住,从而推断出接收操作在发送操作之前执行,导致死锁。

解决方法

  1. 调整操作顺序:在上述例子中,可以先启动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)
}
  1. 使用带缓冲通道:可以将无缓冲通道改为带缓冲通道,这样发送操作不会立即阻塞,直到缓冲区满。例如:
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)
}

这样即使接收操作稍后执行,发送操作也能正常完成。