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

channel

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

上一课,goroutine 算出的结果只能写进一个变量,main 靠 time.Sleep 猜它什么时候做完。channel 是更好的办法:它像一根管子,一个 goroutine 从一头放进一个值,另一个 goroutine 从另一头取出来。

prog.go
1package main2 3import "fmt"4 5func main() {6	ch := make(chan string)7	go func() {8		ch <- "photo.jpg 下好了"9	}()10	msg := <-ch11	fmt.Println(msg)12}
输出
photo.jpg 下好了
代码意思
make(chan string)建一个能传 string 的 channel。
ch <- "…"发送:把值放进 channel。箭头指向 channel。
msg := <-ch接收:从 channel 取出一个值。箭头从 channel 指出来。

channel 的类型写成 chan 元素类型:chan int、chan string、chan bool。它和 map 一样要先 make 才能用。没 make 的 channel 是 nil,往它发送或从它接收都会永远等下去(不是 panic)。

接收会等

<-ch 的时候如果还没有值,接收的一方就停在那里等,直到有人发送。下面 total 在另一个 goroutine 里算订单的合计,main 等着收:

prog.go
1package main2 3import "fmt"4 5func total(prices []int, out chan int) {6	sum := 07	for _, p := range prices {8		sum += p9	}10	out <- sum11}12 13func main() {14	out := make(chan int)15	go total([]int{12, 30, 8}, out)16	fmt.Println("等合计……")17	fmt.Println("合计", <-out)18}
输出
等合计……
合计 50

点单步:走到 <-out 时,main 那条泳道变成「卡在接收」,一根虚线箭头指着 out,说明它在等哪个 channel。这时 total 才轮到,算完合计发出去,main 收到值,接着往下走。不用猜时间:结果一到,main 马上就走,不早也不晚。

发送也会等

反过来也一样。make(chan T) 建的 channel 里面没有地方存值,所以发送的一方要等到有人来接收,两边碰上了,值才交过去。厨房做好一道菜,要等服务员来取:

prog.go
1package main2 3import (4	"fmt"5	"time"6)7 8func main() {9	dishes := make(chan string)10	go func() {11		fmt.Println("厨房:番茄炒蛋做好了")12		dishes <- "番茄炒蛋"13	}()14	time.Sleep(time.Second)15	fmt.Println("服务员:来取菜")16	dish := <-dishes17	fmt.Println("服务员:端走了", dish)18}
输出
厨房:番茄炒蛋做好了
服务员:来取菜
服务员:端走了 番茄炒蛋

点单步,看厨房那条泳道:它显示完第一行,走到 dishes <- "番茄炒蛋" 就变成「卡在发送」,在那里一直等到服务员(main)睡醒、走到 <-dishes。(网页上的 Sleep 不真的等。)

用 channel 等 goroutine 做完

很多时候不需要传什么结果,只想知道「做完了」。那就让 goroutine 做完以后往一个 channel 发一个值,main 收到就知道它做完了:

prog.go
1package main2 3import "fmt"4 5func download(file string, done chan bool) {6	fmt.Println("下好了", file)7	done <- true8}9 10func main() {11	done := make(chan bool)12	go download("photo.jpg", done)13	<-done14	fmt.Println("main 结束")15}
输出
下好了 photo.jpg
main 结束

<-done 单独一行:收到的值不要了,只是等。和上一课的 time.Sleep 比一比:download 做多久,main 就正好等多久。而且 下好了 一定在 main 结束 前面:goroutine 发送之前做的事,main 收到以后一定都已经做完了。

收好几个结果

几个工人一起算平方,都往同一个 channel 里发。main 收几次,就加几次:

prog.go
1package main2 3import "fmt"4 5func main() {6	nums := []int{3, 5, 8, 10}7	results := make(chan int)8	for _, n := range nums {9		go func() {10			results <- n * n11		}()12	}13	sum := 014	for range len(nums) {15		sum += <-results16	}17	fmt.Println("平方和", sum)18}
输出
平方和 198

四个结果到达的先后在真的电脑上每次可能不一样,但加起来总是一样。猜一猜:

先猜一猜

这段程序显示什么?

1package main2 3import "fmt"4 5func main() {6	ch := make(chan int)7	go func() {8		ch <- 109	}()10	go func() {11		ch <- 2012	}()13	fmt.Println(<-ch + <-ch)14}

要是想按原来的顺序显示每个结果,可以给每个工人一个自己的 channel,再按顺序一个一个收:

prog.go
1package main2 3import "fmt"4 5func main() {6	nums := []int{3, 5, 8}7	chans := make([]chan int, len(nums))8	for i, n := range nums {9		chans[i] = make(chan int)10		go func() {11			chans[i] <- n * n12		}()13	}14	for i, ch := range chans {15		fmt.Println(nums[i], "的平方是", <-ch)16	}17}
输出
3 的平方是 9
5 的平方是 25
8 的平方是 64

点单步,看三个工人都卡在发送,每根箭头指着自己的 channel;main 先收 chans[0],再收 chans[1]……谁先算完都没关系,显示的顺序就是 nums 的顺序。

死锁

发送要等接收,接收要等发送。要是谁也等不到,会怎么样?

prog.go
1package main2 3import "fmt"4 5func main() {6	ch := make(chan int)7	ch <- 428	fmt.Println(<-ch)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)。

main 停在第 7 行发送,等人来收。可是能收的只有它自己(下一行),别的 goroutine 一个都没有。Go 发现所有 goroutine 都在等、谁也不会再动,就报 fatal error: all goroutines are asleep - deadlock!(所有 goroutine 都睡着了:死锁)。下面的 goroutine 1 [chan send]: 说 main 卡在发送。点单步走到这一行,泳道变成红色的「全都卡住了」。

工人少了也一样:三个工人只发三个值,main 却要收四次,第四次就再也等不到了。死锁的更多样子见第 23 课。

单向 channel

把 channel 传给函数时,可以写明这个函数只发送还是只接收:chan<- int 只能发,<-chan int 只能收。写反了,编译器会拦住:

prog.go
1package main2 3import "fmt"4 5func square(nums []int, out chan<- int) {6	for _, n := range nums {7		out <- n * n8	}9	fmt.Println(<-out)10}11 12func main() {13	out := make(chan int)14	go square([]int{2, 3}, out)15	fmt.Println(<-out, <-out)16}
输出
# command-line-arguments
./prog.go:9:16: invalid operation: cannot receive from send-only channel out (variable of type chan<- int)

发生了什么: out 是只能发的 channel(chan<-),不能从它收。

cannot receive from send-only channel out:out 在 square 里是只能发的。把第 9 行删掉就好了。普通的 chan int 传给 chan<- int 参数时自动变成单向的。

练一练

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

练习 1

两张订单一起算

total 已经写好,会把合计发进 out。在 main 里建两个 channel,开两个 goroutine 分别算两张订单,再收结果显示出来:订单 A 合计 70 和 订单 B 合计 155。

要显示
订单 A 合计 70
订单 B 合计 155
exercise1.go
1package main2 3import "fmt"4 5func total(prices []int, out chan int) {6	sum := 07	for _, p := range prices {8		sum += p9	}10	out <- sum11}12 13func main() {14	orderA := []int{25, 40, 5}15	orderB := []int{120, 35}16	// 开两个 goroutine,再从 channel 收合计17	fmt.Println("订单 A 合计", 0)18	fmt.Println("订单 B 合计", 0)19}
输出
点「运行」或按 Ctrl+Enter
练习 2

修好它:多收了一次

sumOfSquares 给每个数开一个工人算平方,再把结果加起来。第一组数没问题,第二组却死锁了。改好它,让它显示 30 和 77。

要显示
30
77
exercise2.go
1package main2 3import "fmt"4 5func sumOfSquares(nums []int) int {6	results := make(chan int)7	for _, n := range nums {8		go func() {9			results <- n * n10		}()11	}12	sum := 013	for range 4 {14		sum += <-results15	}16	return sum17}18 19func main() {20	fmt.Println(sumOfSquares([]int{1, 2, 3, 4}))21	fmt.Println(sumOfSquares([]int{4, 5, 6}))22}
输出
点「运行」或按 Ctrl+Enter
练习 3

按顺序报文件大小

给每个文件开一个 goroutine,把字节数换成 KB(除以 1024),发进它自己的 channel;main 按文件的顺序收,显示成 photo.jpg 2048 KB 这样,一个文件一行。

要显示
photo.jpg 2048 KB
song.mp3 5120 KB
notes.txt 10 KB
exercise3.go
1package main2 3import "fmt"4 5func main() {6	files := []string{"photo.jpg", "song.mp3", "notes.txt"}7	sizes := []int{2097152, 5242880, 10240}8	chans := make([]chan int, len(files))9	// 给每个文件建 channel、开 goroutine10	for i := range chans {11		fmt.Println(files[i], sizes[i], "KB")12	}13}
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • ch := make(chan int) 建 channel;ch <- v 发送,v := <-ch 接收。
  • 没有缓冲的 channel:接收要等到有人发,发送要等到有人收,两边碰上才交值。
  • 用 channel 等 goroutine 做完:done <- true 和 <-done,不用猜时间。
  • 几个工人往一个 channel 发:收几次就加几次;要按顺序,每个工人一个 channel,按顺序收。
  • 谁也等不到谁:fatal error: all goroutines are asleep - deadlock!。
  • 单向 channel:chan<- int 只能发,<-chan int 只能收。
  • 单步时泳道写着「卡在发送」「卡在接收」,虚线箭头指着它等的 channel。
这一课学完了吗?点一下标记。进度存在这个浏览器里,随时可以撤销。