MST

星途 面试题库

面试题:Go中如何检测并预防Goroutine因死锁导致的卡住

死锁是导致Goroutine卡住的常见原因之一。在复杂的Go程序中,可能存在多个Goroutine通过通道相互通信并共享资源,容易形成死锁。请描述你如何在代码设计阶段预防死锁,以及在运行时如何检测死锁,并提供相关的代码片段或工具说明。
47.4万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

代码设计阶段预防死锁

  1. 避免循环依赖:确保资源的获取顺序一致,避免形成循环等待。例如,在多个Goroutine中获取锁或通道时,都按照相同的顺序进行操作。
// 错误示例,可能导致死锁
// var mu1 sync.Mutex
// var mu2 sync.Mutex
// 
// func goroutine1() {
//     mu1.Lock()
//     defer mu1.Unlock()
//     mu2.Lock()
//     defer mu2.Unlock()
//     // 业务逻辑
// }
// 
// func goroutine2() {
//     mu2.Lock()
//     defer mu2.Unlock()
//     mu1.Lock()
//     defer mu1.Unlock()
//     // 业务逻辑
// }

// 正确示例,获取锁顺序一致
// func goroutine1() {
//     mu1.Lock()
//     defer mu1.Unlock()
//     mu2.Lock()
//     defer mu2.Unlock()
//     // 业务逻辑
// }
// 
// func goroutine2() {
//     mu1.Lock()
//     defer mu1.Unlock()
//     mu2.Lock()
//     defer mu2.Unlock()
//     // 业务逻辑
// }
  1. 合理设计通道使用
    • 避免无缓冲通道双向阻塞:如果使用无缓冲通道进行通信,确保发送和接收操作在不同的Goroutine中同时准备好,否则可能导致死锁。例如,在一个Goroutine中发送数据到无缓冲通道,而另一个Goroutine需要及时接收。
// 错误示例,可能死锁
// ch := make(chan int)
// go func() {
//     ch <- 1
// }()
// // 这里没有其他Goroutine接收数据,会导致发送操作阻塞,形成死锁
  • 设置合理的缓冲通道大小:根据实际需求设置缓冲通道的大小,避免因缓冲区满而导致发送操作阻塞,或缓冲区空而导致接收操作阻塞。
// 合理设置缓冲通道大小
ch := make(chan int, 10)
go func() {
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch)
}()
for val := range ch {
    // 处理接收到的值
}
  1. 使用超时机制:在通道操作上使用select结合time.After设置超时,防止Goroutine因长时间等待而死锁。
ch := make(chan int)
select {
case val := <-ch:
    // 处理接收到的值
case <-time.After(time.Second):
    // 超时处理
}

运行时检测死锁

  1. Go 1.16+的内置支持:Go语言在1.16版本及以后,运行时在检测到死锁时会自动打印详细的死锁信息,包括涉及的Goroutine堆栈跟踪。
package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup
    wg.Add(2)

    var mu1 sync.Mutex
    var mu2 sync.Mutex

    go func() {
        defer wg.Done()
        mu1.Lock()
        defer mu1.Unlock()
        time.Sleep(time.Second)
        mu2.Lock()
        defer mu2.Unlock()
    }()

    go func() {
        defer wg.Done()
        mu2.Lock()
        defer mu2.Unlock()
        time.Sleep(time.Second)
        mu1.Lock()
        defer mu1.Unlock()
    }()

    wg.Wait()
}

当运行这段可能导致死锁的代码时,Go运行时会检测到死锁并输出类似如下信息:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [semacquire]:
sync.runtime_SemacquireMutex(0xc00001a0d8, 0x0, 0x0)
    /usr/local/go/src/runtime/sema.go:71 +0x39
sync.(*Mutex).Lock(0xc00001a0d8)
    /usr/local/go/src/sync/mutex.go:81 +0x99
main.main.func2()
    /home/user/go/src/yourpackage/main.go:22 +0x49
created by main.main
    /home/user/go/src/yourpackage/main.go:19 +0x67

goroutine 6 [semacquire]:
sync.runtime_SemacquireMutex(0xc00001a0e8, 0x0, 0x0)
    /usr/local/go/src/runtime/sema.go:71 +0x39
sync.(*Mutex).Lock(0xc00001a0e8)
    /usr/local/go/src/sync/mutex.go:81 +0x99
main.main.func1()
    /home/user/go/src/yourpackage/main.go:15 +0x49
created by main.main
    /home/user/go/src/yourpackage/main.go:14 +0x4b
  1. 使用go tool trace:可以使用go tool trace工具来分析程序的执行轨迹,它能帮助发现死锁等并发问题。在程序中添加如下代码来生成跟踪数据:
package main

import (
    "context"
    "fmt"
    "os"
    "runtime/trace"
    "sync"
    "time"
)

func main() {
    f, err := os.Create("trace.out")
    if err != nil {
        panic(err)
    }
    defer f.Close()

    err = trace.Start(f)
    if err != nil {
        panic(err)
    }
    defer trace.Stop()

    var wg sync.WaitGroup
    wg.Add(2)

    var mu1 sync.Mutex
    var mu2 sync.Mutex

    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    go func() {
        defer wg.Done()
        select {
        case <-ctx.Done():
            return
        default:
            mu1.Lock()
            defer mu1.Unlock()
            time.Sleep(time.Second)
            mu2.Lock()
            defer mu2.Unlock()
        }
    }()

    go func() {
        defer wg.Done()
        select {
        case <-ctx.Done():
            return
        default:
            mu2.Lock()
            defer mu2.Unlock()
            time.Sleep(time.Second)
            mu1.Lock()
            defer mu1.Unlock()
        }
    }()

    wg.Wait()
}

运行程序生成trace.out文件后,使用go tool trace trace.out命令,在浏览器中打开生成的报告,通过分析Goroutine的状态和执行顺序来发现死锁问题。