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

带缓冲的 channel 和 close

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

上一课的 channel 里没有地方放值:发送的一方要等到有人来收。make 的时候多给一个数,channel 里就有了几个空位,这叫带缓冲的 channel。有空位时,发送放下值就走,不用等人来收:

prog.go
1package main2 3import "fmt"4 5func main() {6	orders := make(chan string, 3)7	orders <- "咖啡"8	orders <- "三明治"9	orders <- "沙拉"10	fmt.Println(<-orders)11	fmt.Println(<-orders)12	fmt.Println(<-orders)13}
输出
咖啡
三明治
沙拉

这里一个 goroutine 都没开,main 自己发三次、收三次,也没有死锁:三个值先存在 channel 的空位里。取出来的顺序和放进去的一样,先放的先取,像排队。点单步,看 orders 画成三个格子,一个个填上,又一个个空出来。

len 和 cap

cap(ch) 是一共有几个空位,len(ch) 是现在存着几个值:

prog.go
1package main2 3import "fmt"4 5func main() {6	queue := make(chan int, 5)7	queue <- 1018	queue <- 1029	fmt.Println(len(queue), cap(queue))10	<-queue11	fmt.Println(len(queue), cap(queue))12	plain := make(chan int)13	fmt.Println(len(plain), cap(plain))14}
输出
2 5
1 5
0 0

上一课那种不带缓冲的 channel,cap 是 0:一个值也存不下,所以发送要等接收。

满了还是要等

空位用完了,发送照样要等,等到有人取走一个、空出位置来。没人来取,就是死锁:

prog.go
1package main2 3import "fmt"4 5func main() {6	orders := make(chan string, 3)7	orders <- "咖啡"8	orders <- "三明治"9	orders <- "沙拉"10	orders <- "蛋糕"11	fmt.Println(<-orders)12}
输出
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main(...)
	/tmp/sandbox/prog.go:10 +0x1d
exit status 2

发生了什么: 死锁:所有 goroutine 都在等,谁也动不了(比如往没人收的 channel 发,或者 Wait 等不到 Done)。

第 4 次发送时三个格子都满了,main 停在第 10 行等空位,可没有别的 goroutine 会来取:fatal error: all goroutines are asleep - deadlock!。

有人在取,就不会卡死,只是发得快的一方要停下来等。厨房一口气做五道菜,出菜口只放得下两盘,服务员一盘一盘端:

prog.go
1package main2 3import "fmt"4 5func main() {6	counter := make(chan string, 2)7	go func() {8		for _, dish := range []string{"汤", "沙拉", "牛排", "米饭", "甜点"} {9			counter <- dish10		}11	}()12	for range 5 {13		fmt.Println("端走", <-counter)14	}15}
输出
端走 汤
端走 沙拉
端走 牛排
端走 米饭
端走 甜点

点单步,看厨房那条泳道:前两道菜放进格子就接着做下一道;格子满了,它就变成「卡在发送」,等服务员端走一盘才放得进去。

close:不会再发了

发送的一方发完了,可以 close(ch) 告诉接收的一方:不会再有新的值了。关了以后,里面剩下的值照样能收;收完了再收,会马上得到零值。用 v, ok := <-ch 能分清是哪一种:

prog.go
1package main2 3import "fmt"4 5func main() {6	ch := make(chan int, 2)7	ch <- 108	ch <- 209	close(ch)10	v, ok := <-ch11	fmt.Println(v, ok)12	v, ok = <-ch13	fmt.Println(v, ok)14	v, ok = <-ch15	fmt.Println(v, ok)16}
输出
10 true
20 true
0 false

ok 是 true:这是真的发过来的值。ok 是 false:channel 已经关了,也已经空了,v 只是零值 0。

for range:收到关闭为止

for v := range ch 一个个地收,channel 关了而且收完了,循环就结束。这是收「不知道有几个」的值最常用的写法:

prog.go
1package main2 3import "fmt"4 5func sendPrices(out chan int) {6	for _, p := range []int{12, 30, 8, 50} {7		out <- p8	}9	close(out)10}11 12func main() {13	prices := make(chan int)14	go sendPrices(prices)15	total := 016	for p := range prices {17		total += p18	}19	fmt.Println("合计", total)20}
输出
合计 100

点单步,看 main 和 sendPrices 一递一接;close(out) 以后,prices 标成关了,main 的 for range 收完最后一个值就走出循环。猜一猜,要是忘了 close 呢?

先猜一猜

这段程序会怎样?

1package main2 3import "fmt"4 5func sendPrices(out chan int) {6	for _, p := range []int{12, 30, 8} {7		out <- p8	}9}10 11func main() {12	prices := make(chan int)13	go sendPrices(prices)14	for p := range prices {15		fmt.Println(p)16	}17	fmt.Println("收完了")18}

往关了的 channel 发

关了的 channel 能收,不能再发。发了就是 panic:

prog.go
1package main2 3import "fmt"4 5func main() {6	orders := make(chan string, 3)7	orders <- "咖啡"8	close(orders)9	orders <- "蛋糕"10	fmt.Println(len(orders))11}
输出
panic: send on closed channel

goroutine 1 [running]:
main.main(...)
	/tmp/sandbox/prog.go:9 +0x1d
exit status 2

发生了什么: channel 已经 close 了,不能再往里发。

panic: send on closed channel:往关了的 channel 发送。同一个 channel 关两次也是 panic(close of closed channel)。所以规矩是:谁发送,谁来关,而且发完最后一个再关。接收的一方不要去关,它不知道发送的一方还发不发。不需要 range 的 channel 也可以不关,用完了 Go 会自己回收。

一个厨师按单做菜

把这一课的东西放在一起:main 把订单放进一个带缓冲的 channel,放完就关;厨师用 range 一单一单地做,做完把一共用的分钟数发回来:

prog.go
1package main2 3import "fmt"4 5func cook(orders <-chan int, done chan<- int) {6	minutes := 07	for m := range orders {8		fmt.Println("做一道菜,用", m, "分钟")9		minutes += m10	}11	done <- minutes12}13 14func main() {15	orders := make(chan int, 10)16	done := make(chan int)17	go cook(orders, done)18	for _, m := range []int{5, 12, 8} {19		orders <- m20	}21	close(orders)22	fmt.Println("一共", <-done, "分钟")23}
输出
做一道菜,用 5 分钟
做一道菜,用 12 分钟
做一道菜,用 8 分钟
一共 25 分钟

orders 有 10 个空位,main 三单一放就去关,不用等厨师;厨师收完三单、看到关了,range 结束,把总数发进 done。cook 的参数写成单向的:只从 orders 收,只往 done 发。

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。

练习 1

过一遍 channel 的总价

写好 total:建一个正好放得下所有价格的带缓冲 channel,把价格都放进去,关掉,再用 for range 取出来加起来。程序会显示两个购物车的总价。

要显示
50
130
exercise1.go
1package main2 3import "fmt"4 5func total(prices []int) int {6	sum := 07	// 建 channel,把价格放进去、关掉、再取出来加8	return sum9}10 11func main() {12	fmt.Println(total([]int{12, 30, 8}))13	fmt.Println(total([]int{99, 1, 25, 5}))14}
输出
点「运行」或按 Ctrl+Enter
练习 2

修好它:等不到头的 range

evens 把 2 到 limit 之间的偶数发出来,sumEvens 收下来加在一起。可程序死锁了。改好它,让它显示 30 和 12。

要显示
30
12
exercise2.go
1package main2 3import "fmt"4 5func evens(limit int, out chan int) {6	for n := 2; n <= limit; n += 2 {7		out <- n8	}9}10 11func sumEvens(limit int) int {12	ch := make(chan int)13	go evens(limit, ch)14	sum := 015	for n := range ch {16		sum += n17	}18	return sum19}20 21func main() {22	fmt.Println(sumEvens(10))23	fmt.Println(sumEvens(7))24}
输出
点「运行」或按 Ctrl+Enter
练习 3

厨师一共忙了多久

写好 cook:用 for range 从 orders 收每道菜要的分钟数,加起来,收完以后发进 done。程序会算两个晚上的单子。

要显示
25
51
exercise3.go
1package main2 3import "fmt"4 5func cook(orders <-chan int, done chan<- int) {6	// 收完所有订单,把总分钟数发进 done7	done <- 08}9 10func busyMinutes(minutes []int) int {11	orders := make(chan int, len(minutes))12	done := make(chan int)13	go cook(orders, done)14	for _, m := range minutes {15		orders <- m16	}17	close(orders)18	return <-done19}20 21func main() {22	fmt.Println(busyMinutes([]int{5, 12, 8}))23	fmt.Println(busyMinutes([]int{20, 15, 10, 6}))24}
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • make(chan T, n) 建一个有 n 个空位的 channel:有空位时发送不用等,满了才等;取出的顺序和放进的一样。
  • len(ch) 是存着几个,cap(ch) 是一共几个空位;不带缓冲的 channel cap 是 0。
  • close(ch):不会再发了。剩下的值还能收,收完再收得到零值。
  • v, ok := <-ch:ok 是 false 说明 channel 关了、空了。
  • for v := range ch 收到关闭为止;忘了 close 就会一直等,变成死锁。
  • 往关了的 channel 发是 panic send on closed channel;谁发送谁来关。
  • 单步时看 channel 的格子怎么填满、发送的一方什么时候「卡在发送」。
这一课学完了吗?点一下标记。进度存在这个浏览器里,随时可以撤销。