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

select

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

厨房同时做汤和米饭,各从自己的 channel 送出来。要是先 <-soup 再 <-rice,米饭早就好了也得等汤。select 能同时等几个 channel,哪个先好就先走哪个:

prog.go
1package main2 3import (4	"fmt"5	"time"6)7 8func prepare(dish string, d time.Duration, out chan string) {9	time.Sleep(d)10	out <- dish11}12 13func main() {14	soup := make(chan string)15	rice := make(chan string)16	go prepare("汤", 300*time.Millisecond, soup)17	go prepare("米饭", 100*time.Millisecond, rice)18	for range 2 {19		select {20		case s := <-soup:21			fmt.Println("端上", s)22		case r := <-rice:23			fmt.Println("端上", r)24		}25	}26}
输出
端上 米饭
端上 汤

select 的样子像 switch,可每个 case 是一次 channel 操作。它先等着,直到某个 case 能走了,就走那一个,执行它下面的代码。prepare 的参数 d 是 time.Duration 类型,表示一段时长;300*time.Millisecond 就是一个 time.Duration。米饭 0.1 秒就好,所以先端上米饭;第二轮只剩汤。

点单步:走到 select,main 那条泳道变成「卡在 select」,两根虚线箭头同时指着 soup 和 rice。米饭送到时,泳道上出现「选了」,一根绿色箭头指着 rice。(网页上的 Sleep 不真的等:大家都在等时,时钟直接拨到下一个到点的时刻。)

default:都没好就不等

select 里可以写一个 default:所有 case 都走不了时,不等,马上执行 default。用它可以「看一眼有没有,没有就算了」:

prog.go
1package main2 3import "fmt"4 5func main() {6	inbox := make(chan string, 1)7	for range 2 {8		select {9		case m := <-inbox:10			fmt.Println("收到", m)11		default:12			fmt.Println("没有新消息")13		}14		inbox <- "新订单"15	}16}
输出
没有新消息
收到 新订单

第一轮 inbox 是空的,收不到,走了 default;第二轮里面有一条,就收到了。default 只在别的 case 都走不了时才走,不管写在第几个。

超时:time.After

time.After(时长) 给你一个 channel,过了这段时间,它会送来一个值。把它放进 select,就是「最多等这么久」:

prog.go
1package main2 3import (4	"fmt"5	"time"6)7 8func download(file string, d time.Duration) string {9	done := make(chan string, 1)10	go func() {11		time.Sleep(d)12		done <- file + " 下好了"13	}()14	select {15	case msg := <-done:16		return msg17	case <-time.After(200 * time.Millisecond):18		return file + " 超时了"19	}20}21 22func main() {23	fmt.Println(download("a.txt", 100*time.Millisecond))24	fmt.Println(download("b.zip", 500*time.Millisecond))25}
输出
a.txt 下好了
b.zip 超时了

a.txt 0.1 秒就下好,比 0.2 秒的限时早;b.zip 要 0.5 秒,0.2 秒一到 time.After 先送来值,就超时了。done 带 1 个缓冲:超时以后没人再收,下载的 goroutine 发送时也能把值放下就走,不会一直卡着。网页上的时钟是假的,不真的等;在你的电脑上会真的等。猜一猜:

先猜一猜

这段程序显示什么?

1package main2 3import (4	"fmt"5	"time"6)7 8func main() {9	select {10	case <-time.After(300 * time.Millisecond):11		fmt.Println("300 毫秒")12	case <-time.After(100 * time.Millisecond):13		fmt.Println("100 毫秒")14	}15}

在 select 里发送

case 也可以是发送:channel 有空位、能发出去,就走这个 case。配上 default,就是「能放就放,放不下就算了」。订单队列只有 2 个空位:

prog.go
1package main2 3import "fmt"4 5func main() {6	queue := make(chan string, 2)7	for _, order := range []string{"A01", "A02", "A03", "A04"} {8		select {9		case queue <- order:10			fmt.Println("收下", order)11		default:12			fmt.Println("排满了,没接", order)13		}14	}15	fmt.Println("队列里有", len(queue), "单")16}
输出
收下 A01
收下 A02
排满了,没接 A03
排满了,没接 A04
队列里有 2 单

前两单放得进去;第三单时两个空位都满了,发送走不了,就走 default,不会卡住。

几个同时能走时

要是几个 case 同时都能走呢?真 Go 会随机挑一个,每次可能不一样:

1a := make(chan string, 1)2b := make(chan string, 1)3a <- "来自 a"4b <- "来自 b"5select {6case m := <-a:7	fmt.Println(m)8case m := <-b:9	fmt.Println(m)10}

在你的电脑上多跑几次,有时显示「来自 a」,有时「来自 b」。网页上的引擎总是挑最前面的那个,所以这里每次都一样,别依赖它。写程序时,不管挑中哪个,结果都要对。

for 加 select

select 常常放在 for 里,一直收,直到收到「完成了」的信号。关了的 channel 永远能收(收到零值),所以 close(finished) 正好当这个信号:

prog.go
1package main2 3import "fmt"4 5func download(progress chan int, finished chan bool) {6	for p := 25; p <= 100; p += 25 {7		progress <- p8	}9	close(finished)10}11 12func main() {13	progress := make(chan int)14	finished := make(chan bool)15	go download(progress, finished)16	for {17		select {18		case p := <-progress:19			fmt.Println("进度", p)20		case <-finished:21			fmt.Println("下载完成")22			return23		}24	}25}
输出
进度 25
进度 50
进度 75
进度 100
下载完成

收到完成信号时用 return 结束 main。注意:在 select 里写 break,跳出的只是 select,不是外面的 for。

要是 download 忘了 close(finished),select 两个 case 都再也等不到:

prog.go
1package main2 3import "fmt"4 5func download(progress chan int, finished chan bool) {6	for p := 50; p <= 100; p += 50 {7		progress <- p8	}9}10 11func main() {12	progress := make(chan int)13	finished := make(chan bool)14	go download(progress, finished)15	for {16		select {17		case p := <-progress:18			fmt.Println("进度", p)19		case <-finished:20			fmt.Println("下载完成")21			return22		}23	}24}
输出
进度 50
进度 100
fatal error: all goroutines are asleep - deadlock!

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

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

又是死锁,这次 goroutine 1 [select]: 说 main 卡在 select。点单步走到最后,看 main 的两根虚线箭头指着两个再也不会有值的 channel,泳道变红。一个 case 都没有的 select {} 也会永远等下去。

练一练

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

练习 1

下载超时就放弃

写好 fetch 里的 select:done 先送来结果就返回它;先过了 0.2 秒,就返回 文件名 超时了。程序会显示两个文件的结果。

要显示
photo.jpg 下好了
movie.mp4 超时了
exercise1.go
1package main2 3import (4	"fmt"5	"time"6)7 8func fetch(file string, d time.Duration) string {9	done := make(chan string, 1)10	go func() {11		time.Sleep(d)12		done <- file + " 下好了"13	}()14	// 在这里写 select15	return ""16}17 18func main() {19	fmt.Println(fetch("photo.jpg", 50*time.Millisecond))20	fmt.Println(fetch("movie.mp4", 900*time.Millisecond))21}
输出
点「运行」或按 Ctrl+Enter
练习 2

队列满了就不接

写好 accept:把订单一个个用 select 往有 room 个空位的队列里放,放得进去就算收下,放不进去就算没接,最后返回收下和没接的单数。

要显示
队列里 2 单
收下 2 单,没接 3 单
队列里 3 单
收下 3 单,没接 0 单
exercise2.go
1package main2 3import "fmt"4 5func accept(orders []string, room int) (int, int) {6	queue := make(chan string, room)7	taken, refused := 0, 08	for range orders {9		// 用 select 往 queue 里放10	}11	fmt.Println("队列里", len(queue), "单")12	return taken, refused13}14 15func main() {16	t, r := accept([]string{"A1", "A2", "A3", "A4", "A5"}, 2)17	fmt.Printf("收下 %d 单,没接 %d 单\n", t, r)18	t, r = accept([]string{"B1", "B2", "B3"}, 4)19	fmt.Printf("收下 %d 单,没接 %d 单\n", t, r)20}
输出
点「运行」或按 Ctrl+Enter
练习 3

修好它:只收到一个

两个 goroutine 分别算两张订单的合计,订单 B 算得快。程序只显示了一张。改好它,让两张都显示,谁先算好谁在前面:B 合计 100,然后 A 合计 50。

要显示
B 合计 100
A 合计 50
exercise3.go
1package main2 3import (4	"fmt"5	"time"6)7 8func total(prices []int, d time.Duration, out chan int) {9	time.Sleep(d)10	sum := 011	for _, p := range prices {12		sum += p13	}14	out <- sum15}16 17func main() {18	a := make(chan int)19	b := make(chan int)20	go total([]int{12, 30, 8}, 300*time.Millisecond, a)21	go total([]int{99, 1}, 100*time.Millisecond, b)22	select {23	case s := <-a:24		fmt.Println("A 合计", s)25	case s := <-b:26		fmt.Println("B 合计", s)27	}28}
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • select 同时等几个 channel 操作,哪个 case 先能走就走哪个,而且只走一个。
  • default:所有 case 都走不了时马上执行,不等。
  • case <-time.After(d): 做超时;网页上的时钟是假的,不真的等。
  • case ch <- v: 也能放进 select;配上 default 就是「放不下就算了」。
  • 几个 case 同时能走时,真 Go 随机挑,程序不能依赖挑中哪个。
  • for 加 select 一直收,close(finished) 当完成信号;select 里的 break 只跳出 select。
  • 单步时「卡在 select」的泳道有几根虚线箭头,「选了」那根是绿色的。
这一课学完了吗?点一下标记。进度存在这个浏览器里,随时可以撤销。