MST

星途 面试题库

面试题:Go语言死锁与资源竞争的关系及调试区别

说明Go语言中死锁和资源竞争这两种情况的联系与区别。当程序疑似出现死锁,但实际可能是资源竞争导致的类似死锁现象时,如何通过不同的调试手段来准确区分并解决这两个问题?
49.4万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

联系与区别

  1. 联系
    • 死锁和资源竞争都与并发编程中的资源管理相关,都可能导致程序出现非预期的行为,影响程序的正确性和性能。
  2. 区别
    • 死锁:是指两个或多个 goroutine 相互等待对方释放资源,从而导致所有这些 goroutine 都无法继续执行的一种状态。例如,goroutine A 持有资源 R1 并等待资源 R2,而 goroutine B 持有资源 R2 并等待资源 R1,这就形成了死锁。死锁通常是由于资源获取顺序不当或者资源分配策略不合理导致的。
    • 资源竞争:当两个或多个 goroutine 同时访问和修改共享资源,而没有适当的同步机制时,就会发生资源竞争。资源竞争可能导致数据的不一致或者程序的不确定性行为,但它不一定会像死锁那样使程序完全停滞。例如,一个 goroutine 读取共享变量,同时另一个 goroutine 正在修改这个变量,就可能出现资源竞争。

调试手段

  1. 区分死锁
    • 使用 runtime 包检测:Go 运行时系统内置了死锁检测机制。当程序发生死锁时,Go 运行时会打印出详细的死锁信息,包括哪些 goroutine 参与了死锁以及它们等待的资源等。例如,在 main 函数中导入 runtime 包,如下代码:
package main

import (
    "fmt"
    "runtime"
)

func main() {
    runtime.SetMutexProfileFraction(1)
    runtime.SetBlockProfileRate(1)
    // 这里是你的并发代码
    select {}
}

运行程序时,如果发生死锁,会在终端输出类似如下信息:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
    /path/to/your/file.go:10 +0x120
exit status 2
  • 分析代码逻辑:仔细审查代码中资源获取和释放的逻辑,特别是锁的使用。检查是否存在循环依赖的资源获取情况,例如上述提到的两个 goroutine 互相等待对方资源的情况。
  1. 区分资源竞争
    • 使用 go tool race:这是 Go 语言专门用于检测资源竞争的工具。在编译和运行程序时带上 -race 标志,例如:
go run -race main.go

如果程序存在资源竞争,go tool race 会输出详细的信息,包括哪个 goroutine 在什么时间访问了共享资源,同时哪个 goroutine 也在访问或修改该资源。例如输出可能如下:

==================
WARNING: DATA RACE
Read at 0x00c0000b0030 by goroutine 6:
  main.worker()
      /path/to/your/file.go:20 +0x100

Previous write at 0x00c0000b0030 by goroutine 5:
  main.worker()
      /path/to/your/file.go:15 +0x140

Goroutine 6 (running) created at:
  main.main()
      /path/to/your/file.go:18 +0x80

Goroutine 5 (finished) created at:
  main.main()
      /path/to/your/file.go:13 +0x60
==================
  • 代码审查:查看共享变量的访问逻辑,确保在访问共享资源时使用了适当的同步机制,如互斥锁(sync.Mutex)、读写锁(sync.RWMutex)或者通道(chan)来协调 goroutine 之间的访问。

解决办法

  1. 解决死锁
    • 调整资源获取顺序:确保所有 goroutine 以相同的顺序获取资源,避免循环依赖。例如,如果有资源 R1 和 R2,所有 goroutine 都先获取 R1 再获取 R2,就不会出现死锁。
    • 使用超时机制:在获取资源时设置超时,避免无限期等待。可以使用 context 包来实现,例如:
package main

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

func main() {
    var mu sync.Mutex
    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    defer cancel()
    go func() {
        mu.Lock()
        defer mu.Unlock()
        // 模拟长时间操作
        time.Sleep(2 * time.Second)
    }()
    select {
    case <-ctx.Done():
        fmt.Println("获取锁超时")
    }
}
  1. 解决资源竞争
    • 使用同步原语:如 sync.Mutex 来保护共享资源。在访问共享资源前加锁,访问完后解锁,例如:
package main

import (
    "fmt"
    "sync"
)

var (
    counter int
    mu      sync.Mutex
)

func increment(wg *sync.WaitGroup) {
    mu.Lock()
    counter++
    mu.Unlock()
    wg.Done()
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go increment(&wg)
    }
    wg.Wait()
    fmt.Println("Final counter:", counter)
}
  • 使用通道:通过通道来传递数据,避免直接共享变量。例如:
package main

import (
    "fmt"
    "sync"
)

func producer(ch chan int, wg *sync.WaitGroup) {
    for i := 0; i < 10; i++ {
        ch <- i
    }
    close(ch)
    wg.Done()
}

func consumer(ch chan int, wg *sync.WaitGroup) {
    for val := range ch {
        fmt.Println("Consumed:", val)
    }
    wg.Done()
}

func main() {
    var wg sync.WaitGroup
    ch := make(chan int)
    wg.Add(2)
    go producer(ch, &wg)
    go consumer(ch, &wg)
    wg.Wait()
}