死锁和常见的坑
每个 goroutine 都停下来在等,而它们等的东西谁也给不了,程序就再也动不了了。这叫死锁。Go 发现所有 goroutine 都在等时,会停下程序,显示:
fatal error: all goroutines are asleep - deadlock!
意思是「所有 goroutine 都睡着了」。这一课把最常见的几种死锁都跑一遍。每一段都可以点单步,或者点「看看是怎么出错的」:每个 goroutine 一条泳道,看最后谁停在哪一行、在等哪个 channel 或哪把锁。
没人接收的发送
1package main2 3import "fmt"4 5func main() {6 orders := make(chan string)7 orders <- "订单 1"8 fmt.Println(<-orders)9}
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main(...)
/tmp/sandbox/prog.go:7 +0x1d
exit status 2发生了什么: 死锁:所有 goroutine 都在等,谁也动不了(比如往没人收的 channel 发,或者 Wait 等不到 Done)。
没有缓冲的 channel,发送要等到有人接收(第 19 课)。可是接收写在下一行,而 main 卡在第 7 行的发送上,永远走不到下一行。程序里只有 main 一个 goroutine,它在等,就没人能动了。
报错下面的 goroutine 1 [chan send]: 说的是:1 号 goroutine(就是 main)停在「往 channel 发送」上。改法有两种:让另一个 goroutine 来发(或来收),或者给 channel 一个缓冲,make(chan string, 1)(第 20 课)。
忘了 close
for v := range ch 一直收,直到 channel 被关上。发的那边发完了却没有 close,range 就会一直等下一个:
1package main2 3import "fmt"4 5func main() {6 prices := make(chan int)7 go func() {8 for _, p := range []int{30, 45, 25} {9 prices <- p10 }11 }()12 total := 013 for p := range prices {14 total += p15 fmt.Println("收到", p)16 }17 fmt.Println("合计", total)18}
收到 30
收到 45
收到 25
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main(...)
/tmp/sandbox/prog.go:13 +0x1d
exit status 2发生了什么: 死锁:所有 goroutine 都在等,谁也动不了(比如往没人收的 channel 发,或者 Wait 等不到 Done)。
三个价格都收到了,然后死锁,停在第 13 行的 range。点单步走到最后:发价格的那条泳道已经结束了,只剩 main 停在 prices 上,等一个永远不会来的值。
发的那边发完了就 close。最稳的写法是在 goroutine 一开头写 defer close(prices):
1package main2 3import "fmt"4 5func main() {6 prices := make(chan int)7 go func() {8 defer close(prices)9 for _, p := range []int{30, 45, 25} {10 prices <- p11 }12 }()13 total := 014 for p := range prices {15 total += p16 }17 fmt.Println("合计", total)18}
合计 100
记住:谁发,谁关。收的那边不要关 channel,因为它不知道发的那边还要不要发。
忘了 Done
wg.Wait() 等计数回到 0。有一个 goroutine 没调 Done,计数就永远不是 0:
1package main2 3import (4 "fmt"5 "sync"6)7 8func main() {9 sizes := []int{1200, 0, 800}10 thumbs := make([]int, len(sizes))11 var wg sync.WaitGroup12 for i, s := range sizes {13 wg.Add(1)14 go func() {15 if s == 0 {16 return17 }18 thumbs[i] = s / 1019 wg.Done()20 }()21 }22 wg.Wait()23 fmt.Println(thumbs)24}
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [sync.WaitGroup.Wait]:
main.main(...)
/tmp/sandbox/prog.go:22 +0x1d
exit status 2发生了什么: 死锁:所有 goroutine 都在等,谁也动不了(比如往没人收的 channel 发,或者 Wait 等不到 Done)。
大小是 0 的那张图片提前 return 了,跳过了 wg.Done()。计数停在 1,main 停在第 22 行的 wg.Wait()。所以上一课说:defer wg.Done() 写在 goroutine 的第一行,从哪里结束都会执行。
另一种忘了 Done:把 WaitGroup 传给函数时传的是复制,不是指针。
1package main2 3import (4 "fmt"5 "sync"6)7 8func check(order string, wg sync.WaitGroup) {9 defer wg.Done()10 fmt.Println("检查", order)11}12 13func main() {14 var wg sync.WaitGroup15 wg.Add(1)16 go check("订单 7", wg)17 wg.Wait()18 fmt.Println("都检查完了")19}
检查 订单 7
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [sync.WaitGroup.Wait]:
main.main(...)
/tmp/sandbox/prog.go:17 +0x1d
exit status 2发生了什么: 死锁:所有 goroutine 都在等,谁也动不了(比如往没人收的 channel 发,或者 Wait 等不到 Done)。
check 做完了,也调了 Done,可那是复制里的计数。main 里的 wg 还是 1。参数要写成 wg *sync.WaitGroup,调用时传 &wg。go vet 会提醒这种写法(passes lock by value:WaitGroup 不能复制)。
同一把锁锁两次
1package main2 3import (4 "fmt"5 "sync"6)7 8type Cart struct {9 mu sync.Mutex10 items []int11}12 13func (c *Cart) Total() int {14 c.mu.Lock()15 defer c.mu.Unlock()16 sum := 017 for _, p := range c.items {18 sum += p19 }20 return sum21}22 23func (c *Cart) Add(p int) {24 c.mu.Lock()25 defer c.mu.Unlock()26 c.items = append(c.items, p)27 fmt.Println("加了", p, "合计", c.Total())28}29 30func main() {31 c := &Cart{}32 c.Add(20)33}
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [sync.Mutex.Lock]:
main.(*Cart).Total(...)
/tmp/sandbox/prog.go:14 +0x1d
main.(*Cart).Add(...)
/tmp/sandbox/prog.go:27 +0x1d
main.main(...)
/tmp/sandbox/prog.go:32 +0x1d
exit status 2发生了什么: 死锁:所有 goroutine 都在等,谁也动不了(比如往没人收的 channel 发,或者 Wait 等不到 Done)。
Add 拿着锁,又调用了 Total,Total 再去 Lock 同一把锁。锁在 Add 手里,要等 Add 解锁;Add 又在等 Total 返回。自己等自己,停在第 14 行。Go 的 Mutex 不记得是谁锁的:同一个 goroutine 再锁一次,一样要等。
改法:把「不加锁」的计算写成一个小写的辅助方法(比如 total()),Total 和 Add 都在自己拿到锁以后调用它。
这段程序会怎样?
1package main2 3import "fmt"4 5func main() {6 ch := make(chan int, 2)7 ch <- 18 ch <- 29 ch <- 310 fmt.Println(<-ch, <-ch, <-ch)11}
缓冲只有 2 格。前两个值放进缓冲,第三次发送要等有人取走一个,可是取的代码在后面,main 走不到。只有一个 goroutine,它在等,就是死锁。
多了一次 Done、多了一次 Unlock
反过来,Done 比 Add 多,计数变成负数,程序马上 panic:
1package main2 3import (4 "fmt"5 "sync"6)7 8func main() {9 orders := []string{"订单 1", "订单 2", "订单 3"}10 var wg sync.WaitGroup11 wg.Add(2)12 for _, o := range orders {13 fmt.Println("处理", o)14 wg.Done()15 }16 wg.Wait()17}
处理 订单 1
处理 订单 2
处理 订单 3
panic: sync: negative WaitGroup counter
goroutine 1 [running]:
main.main(...)
/tmp/sandbox/prog.go:14 +0x1d
exit status 2发生了什么: Done() 比 Add() 多:计数变成负数了。
三个订单只 Add 了 2。第三次 Done 让计数变成 -1:sync: negative WaitGroup counter。用 wg.Add(len(orders)),或者每次 go 之前 wg.Add(1),数就不会错。
锁也一样:Unlock 一把没锁着的 Mutex,程序停下,显示 fatal error: sync: unlock of unlocked mutex。常见的原因是写了 defer mu.Unlock(),后面又手动 Unlock 了一次。
卡住却不报错
只有所有 goroutine 都在等时,Go 才报死锁。如果 main 还能跑完,卡住的 goroutine 不会报错,只是悄悄地永远停在那里:
1func main() {2 results := make(chan int)3 go func() {4 results <- 42 // 没人收:这个 goroutine 永远停在这里5 }()6 fmt.Println("不等结果就走了")7}
这段程序正常结束,什么错也不报。在一个短程序里没关系;在一直运行的服务器里,这样卡住的 goroutine 越积越多,会占满内存。这叫 goroutine 泄漏。每开一个 goroutine,都想一想:它怎么结束?
排查清单
看到 all goroutines are asleep,先看报错里 main 停在哪一行,再点单步看别的泳道在等什么:
main 停在 | 常见原因 | 改法 |
|---|---|---|
ch <- v | 没人接收 | 另开 goroutine 收,或者给缓冲 |
for v := range ch | 发的那边没 close | 发完 close(ch),最好 defer close(ch) |
wg.Wait() | 有 goroutine 没 Done,或者 WaitGroup 传了复制 | defer wg.Done(),传 &wg |
mu.Lock() | 锁没还,或者同一个 goroutine 锁了两次 | defer mu.Unlock(),锁里不调用会再锁的函数 |
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。这三段程序现在都会死锁。
修好它:没人收的价格
✓main 想先把价格都发进 channel,再一个个收出来加总,可是第一次发送就卡住了。让程序显示两张价格单的合计。(main 的代码不用改。)
100 20
1package main2 3import "fmt"4 5func sum(prices []int) int {6 ch := make(chan int)7 for _, p := range prices {8 ch <- p9 }10 total := 011 for range prices {12 total += <-ch13 }14 return total15}16 17func main() {18 fmt.Println(sum([]int{30, 45, 25}))19 fmt.Println(sum([]int{12, 8}))20}
修好它:取消的订单让 Wait 等不完
✓每个订单一个 goroutine,算出要付的钱(数量 × 单价);数量是 0 的订单已经取消,跳过。可是程序卡在 wg.Wait() 上。让它显示每个订单的金额。
[30 0 75 0 15]
1package main2 3import (4 "fmt"5 "sync"6)7 8func main() {9 qty := []int{2, 0, 5, 0, 1}10 price := 1511 amounts := make([]int, len(qty))12 var wg sync.WaitGroup13 for i, q := range qty {14 wg.Add(1)15 go func() {16 if q == 0 {17 return18 }19 amounts[i] = q * price20 wg.Done()21 }()22 }23 wg.Wait()24 fmt.Println(amounts)25}
修好它:锁里又锁的购物车
✓Add 拿着锁调用 Total,Total 又去锁同一把锁。加一个不加锁的方法 total() 来算合计,让 Total 和 Add 都用它。程序往购物车里加了三样东西。
加了 20 合计 20 加了 35 合计 55 加了 5 合计 60 最后 60
1package main2 3import (4 "fmt"5 "sync"6)7 8type Cart struct {9 mu sync.Mutex10 items []int11}12 13func (c *Cart) Total() int {14 c.mu.Lock()15 defer c.mu.Unlock()16 sum := 017 for _, p := range c.items {18 sum += p19 }20 return sum21}22 23func (c *Cart) Add(p int) {24 c.mu.Lock()25 defer c.mu.Unlock()26 c.items = append(c.items, p)27 fmt.Println("加了", p, "合计", c.Total())28}29 30func main() {31 c := &Cart{}32 c.Add(20)33 c.Add(35)34 c.Add(5)35 fmt.Println("最后", c.Total())36}
这一课学会了
- 所有 goroutine 都在等、谁也动不了,就是死锁:
fatal error: all goroutines are asleep - deadlock!。报错里有main停在哪一行,单步能看到每条泳道在等什么。 - 往没有缓冲的 channel 发送,要有另一个 goroutine 接收。
range一个 channel,发的那边发完要close:谁发,谁关。defer wg.Done()写在 goroutine 第一行;WaitGroup 传指针。- Go 的
Mutex同一个 goroutine 也不能锁两次;锁里要调用的计算,写成不加锁的辅助方法。 Done比Add多是 panic,Unlock没锁着的锁是 fatal error。- 只有一部分 goroutine 卡住时不报错,它们悄悄泄漏:每个 goroutine 都要有办法结束。