Kodewick
开始学 →
第 23 课,共 24 课目录

死锁和常见的坑

第 23 课,共 24 课 · 第 5 部分 · 并发 · 约 15 分钟

每个 goroutine 都停下来在等,而它们等的东西谁也给不了,程序就再也动不了了。这叫死锁。Go 发现所有 goroutine 都在等时,会停下程序,显示:

fatal error: all goroutines are asleep - deadlock!

意思是「所有 goroutine 都睡着了」。这一课把最常见的几种死锁都跑一遍。每一段都可以点单步,或者点「看看是怎么出错的」:每个 goroutine 一条泳道,看最后谁停在哪一行、在等哪个 channel 或哪把锁。

没人接收的发送

prog.go
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 就会一直等下一个:

prog.go
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):

prog.go
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:

prog.go
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 传给函数时传的是复制,不是指针。

prog.go
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 不能复制)。

同一把锁锁两次

prog.go
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}

多了一次 Done、多了一次 Unlock

反过来,Done 比 Add 多,计数变成负数,程序马上 panic:

prog.go
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(),锁里不调用会再锁的函数

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。这三段程序现在都会死锁。

练习 1

修好它:没人收的价格

main 想先把价格都发进 channel,再一个个收出来加总,可是第一次发送就卡住了。让程序显示两张价格单的合计。(main 的代码不用改。)

要显示
100
20
exercise1.go
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}
输出
点「运行」或按 Ctrl+Enter
练习 2

修好它:取消的订单让 Wait 等不完

每个订单一个 goroutine,算出要付的钱(数量 × 单价);数量是 0 的订单已经取消,跳过。可是程序卡在 wg.Wait() 上。让它显示每个订单的金额。

要显示
[30 0 75 0 15]
exercise2.go
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}
输出
点「运行」或按 Ctrl+Enter
练习 3

修好它:锁里又锁的购物车

Add 拿着锁调用 Total,Total 又去锁同一把锁。加一个不加锁的方法 total() 来算合计,让 Total 和 Add 都用它。程序往购物车里加了三样东西。

要显示
加了 20 合计 20
加了 35 合计 55
加了 5 合计 60
最后 60
exercise3.go
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}
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • 所有 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 都要有办法结束。
这一课学完了吗?点一下标记。进度存在这个浏览器里,随时可以撤销。