序文
最近、デッドロック コードが送信され、特定の機能が使用できなくなりました。フロントエンド担当者は、フィードバックについて疑問に思っていました。昨日は機能しましたが、今日はなぜ機能しないのでしょうか?
改めて見てみると行き詰まりでした。
質問
- コードはセルフテストされておらず、非常に単純な修正で問題は発生しないと思いました
bug
。 - git commit 後の CI はデッドロックをチェックしません。
公式はデッドロック検出を提供していますか?
解決する
インターネットで検索したところ、公式 Web サイトにはデッドロック検出がないことがわかりました。今日の主役であるランタイム コード検出をベースにしたhttps://github.com/sasha-s/go-deadlockを紹介しましょう。
deadlock
標準ライブラリの同期プリミティブではなく、sync
標準ライブラリの同期プリミティブを使用することでコードに反映されます。
まずは簡単な使用例を見てみましょう。
// 测试 deadlock 的锁检测超时时间
// 同一把锁在不同的协程中去获取
func tGoDeadlock1() {
start := time.Now()
defer func() {
fmt.Println("耗时:", time.Now().Sub(start))
}()
var wg sync.WaitGroup
wg.Add(1)
deadlock.Opts.DeadlockTimeout = time.Second // 获取锁超时控制
//deadlock.Opts.Disable = true // 生产环境可以将其设置为 true
var mu deadlock.Mutex
mu.Lock()
go func() {
mu.Lock()
defer mu.Unlock()
fmt.Println("i'm in goroutine")
wg.Done()
}()
time.Sleep(time.Second * 15)
mu.Unlock()
wg.Wait()
}
2 つのコルーチンが同じロックを同時に取得します。この時点でデッドロックが発生し、プロセスが終了しないことは誰もが知っています。ただし、上記のコードの出力は次のとおりです。
単純明快で、ロックの試行にかかる時間が超過しているとpanic
言われています。_ ここで追加したのは、取得ロック超過検出を として設定することです。panic
1s
deadlock.Opts.DeadlockTimeout = time.Second
1s
ソースコード分析
ソース コードを開くと、その原理はそれほど複雑ではなく、簡単に言うと、ロック時にこのメソッドがトリガーされ、lock
競合するロック関係があるかどうかを検出します。lockOrder
ロックされたメモリ情報はによって保存され、その構造は次のとおりです。
type lockOrder struct {
mu sync.Mutex
cur map[interface{
}]stackGID // stacktraces + gids for the locks currently taken.
order map[beforeAfter]ss // expected order of locks.
}
type stackGID struct {
stack []uintptr
gid int64
}
type beforeAfter struct {
before interface{
}
after interface{
}
}
type ss struct {
before []uintptr
after []uintptr
}
具体的なプロセスを確認するために、ソース コードの一部を投稿してみましょう。
ロックを取得します。
func (m *Mutex) Lock() {
lock(m.mu.Lock, m)
}
func lock(lockFn func(), ptr interface{
}) {
if Opts.Disable {
lockFn()
return
}
stack := callers(1)
preLock(stack, ptr)
if Opts.DeadlockTimeout <= 0 {
lockFn()
} else {
ch := make(chan struct{
})
currentID := goid.Get()
go func() {
for {
t := time.NewTimer(Opts.DeadlockTimeout)
defer t.Stop() // This runs after the losure finishes, but it's OK.
select {
case <-t.C:
lo.mu.Lock()
prev, ok := lo.cur[ptr]
if !ok {
lo.mu.Unlock()
break // Nobody seems to be holding the lock, try again.
}
Opts.mu.Lock()
fmt.Fprintln(Opts.LogBuf, header)
fmt.Fprintln(Opts.LogBuf, "Previous place where the lock was grabbed")
fmt.Fprintf(Opts.LogBuf, "goroutine %v lock %p\n", prev.gid, ptr)
printStack(Opts.LogBuf, prev.stack)
fmt.Fprintln(Opts.LogBuf, "Have been trying to lock it again for more than", Opts.DeadlockTimeout)
fmt.Fprintf(Opts.LogBuf, "goroutine %v lock %p\n", currentID, ptr)
printStack(Opts.LogBuf, stack)
stacks := stacks()
grs := bytes.Split(stacks, []byte("\n\n"))
for _, g := range grs {
if goid.ExtractGID(g) == prev.gid {
fmt.Fprintln(Opts.LogBuf, "Here is what goroutine", prev.gid, "doing now")
Opts.LogBuf.Write(g)
fmt.Fprintln(Opts.LogBuf)
}
}
lo.other(ptr)
if Opts.PrintAllCurrentGoroutines {
fmt.Fprintln(Opts.LogBuf, "All current goroutines:")
Opts.LogBuf.Write(stacks)
}
fmt.Fprintln(Opts.LogBuf)
if buf, ok := Opts.LogBuf.(*bufio.Writer); ok {
buf.Flush()
}
Opts.mu.Unlock()
lo.mu.Unlock()
Opts.OnPotentialDeadlock()
<-ch
return
case <-ch:
return
}
}
}()
lockFn()
postLock(stack, ptr)
close(ch)
return
}
postLock(stack, ptr)
}
func preLock(stack []uintptr, p interface{
}) {
lo.preLock(stack, p)
}
func (l *lockOrder) preLock(stack []uintptr, p interface{
}) {
if Opts.DisableLockOrderDetection {
return
}
gid := goid.Get()
l.mu.Lock()
for b, bs := range l.cur {
if b == p {
if bs.gid == gid {
Opts.mu.Lock()
fmt.Fprintln(Opts.LogBuf, header, "Recursive locking:")
fmt.Fprintf(Opts.LogBuf, "current goroutine %d lock %p\n", gid, b)
printStack(Opts.LogBuf, stack)
fmt.Fprintln(Opts.LogBuf, "Previous place where the lock was grabbed (same goroutine)")
printStack(Opts.LogBuf, bs.stack)
l.other(p)
if buf, ok := Opts.LogBuf.(*bufio.Writer); ok {
buf.Flush()
}
Opts.mu.Unlock()
Opts.OnPotentialDeadlock()
}
continue
}
if bs.gid != gid {
// We want locks taken in the same goroutine only.
continue
}
if s, ok := l.order[beforeAfter{
p, b}]; ok {
Opts.mu.Lock()
fmt.Fprintln(Opts.LogBuf, header, "Inconsistent locking. saw this ordering in one goroutine:")
fmt.Fprintln(Opts.LogBuf, "happened before")
printStack(Opts.LogBuf, s.before)
fmt.Fprintln(Opts.LogBuf, "happened after")
printStack(Opts.LogBuf, s.after)
fmt.Fprintln(Opts.LogBuf, "in another goroutine: happened before")
printStack(Opts.LogBuf, bs.stack)
fmt.Fprintln(Opts.LogBuf, "happened after")
printStack(Opts.LogBuf, stack)
l.other(p)
fmt.Fprintln(Opts.LogBuf)
if buf, ok := Opts.LogBuf.(*bufio.Writer); ok {
buf.Flush()
}
Opts.mu.Unlock()
Opts.OnPotentialDeadlock()
}
l.order[beforeAfter{
b, p}] = ss{
bs.stack, stack}
if len(l.order) == Opts.MaxMapSize {
// Reset the map to keep memory footprint bounded.
l.order = map[beforeAfter]ss{
}
}
}
l.mu.Unlock()
}
ロックを解除します。
func (m *Mutex) Unlock() {
m.mu.Unlock()
if !Opts.Disable {
postUnlock(m)
}
}
func postUnlock(p interface{
}) {
lo.postUnlock(p)
}
func (l *lockOrder) postUnlock(p interface{
}) {
l.mu.Lock()
delete(l.cur, p)
l.mu.Unlock()
}
要約する
go-deadlock は、公式標準ライブラリの 、 、 、 などをカプセル化し、Mutex
メソッドCond
にOnce
デッドPool
ロック判定ロジックの層を追加したもので、ソースコードが少ないので独学でも学習可能です。WaitGroup
Lock
注:運用環境では直接使用しないことをお勧めします。忘れずに設定してください。deadlock
deadlock.Opts.Disable = true
関連するコードは、私のGithub/GoTest ウェアハウスsyncTest
のディレクトリにあるファイルにあります。go-deadlock.go
このプロジェクトは、主に日々の学習の記録とテストに関するものです。興味があれば、ご覧ください。