面试题答案
一键面试使用标准库工具检测死锁
Go 语言标准库中的 runtime
包提供了死锁检测功能。当程序发生死锁时,运行时系统会自动检测到并输出详细的错误信息,包括死锁发生的位置和涉及的 goroutine 堆栈信息。例如:
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
mu.Lock()
go func() {
mu.Lock()
fmt.Println("This will not be printed")
mu.Unlock()
}()
mu.Unlock()
}
运行上述代码,当发生死锁时,Go 运行时会打印类似如下信息:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [semacquire]:
sync.runtime_SemacquireMutex(0xc00001c080, 0x0, 0x0)
/usr/local/go/src/runtime/sema.go:713 +0x4e
sync.(*Mutex).Lock(0xc00001c080)
/usr/local/go/src/sync/mutex.go:81 +0x9c
main.main()
/Users/user/go/src/demo/main.go:8 +0x109
goroutine 3 [semacquire]:
sync.runtime_SemacquireMutex(0xc00001c080, 0x0, 0x0)
/usr/local/go/src/runtime/sema.go:713 +0x4e
sync.(*Mutex).Lock(0xc00001c080)
/usr/local/go/src/sync/mutex.go:81 +0x9c
main.main.func1()
/Users/user/go/src/demo/main.go:10 +0x42
created by main.main
/Users/user/go/src/demo/main.go:9 +0xf6
自己实现简单死锁检测机制
数据结构设计
- 记录锁状态的结构体:
type LockStatus struct {
locked bool
owner int
waitList []int
}
locked
表示锁当前是否被锁定。owner
记录当前持有锁的 goroutine ID(可以通过runtime.GoID()
获取)。waitList
是等待获取锁的 goroutine ID 列表。
- 全局锁状态映射:
var lockStates = make(map[*sync.Mutex]LockStatus)
用于存储所有需要检测的互斥锁的状态。
检测逻辑
- 获取锁前:
- 在获取锁(
mu.Lock()
)之前,记录当前 goroutine ID,检查锁的状态。 - 如果锁已被锁定,将当前 goroutine ID 添加到等待列表
waitList
中。
- 在获取锁(
- 获取锁后:
- 获取锁成功后(
mu.Lock()
调用返回),更新锁状态,设置locked
为true
,owner
为当前 goroutine ID,并清空waitList
。
- 获取锁成功后(
- 释放锁时:
- 在释放锁(
mu.Unlock()
)时,检查锁的owner
是否为当前 goroutine ID,如果是,将locked
设置为false
,并从等待列表中取出第一个 goroutine ID,假设为nextGoroutineID
,标记其可以获取锁。
- 在释放锁(
- 死锁检测:
- 可以定期(例如通过一个定时 goroutine)检查
lockStates
中的所有锁状态。 - 如果某个锁处于锁定状态,且等待列表不为空,并且等待列表中的 goroutine 所等待的锁又被其他等待列表中的 goroutine 持有,形成了循环等待,就判定为死锁。
- 可以定期(例如通过一个定时 goroutine)检查
优缺点
- 优点:
- 定制性:可以根据具体需求定制检测逻辑和处理方式,例如可以更精细地控制检测频率、输出更符合业务需求的死锁报告等。
- 深入理解:实现过程有助于深入理解死锁原理和 Go 语言并发编程机制。
- 缺点:
- 复杂性:实现相对复杂,需要处理各种边界情况,如 goroutine 异常退出时对锁状态的清理等。
- 性能开销:定期检测会带来一定的性能开销,尤其是在高并发场景下,频繁的状态检查可能影响程序的整体性能。
- 局限性:只能检测自定义管理的锁的死锁情况,对于标准库或其他未纳入检测范围的锁无法检测。