Files
the-way-to-go_ZH_CN/eBook/14.4.md
marjune 6ed73bf4f5 Move images (#704)
* refactor: merge image directories into one

```bash
git mv -v images/* eBook/images/
sed -i -e 's;../images;images;g' eBook/*.md
```

* use correct chapter number for image names in 4.9

* use correct chapter number for image names in 7.2.4
2019-08-04 00:22:43 -07:00

188 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 14.4 使用 select 切换协程
从不同的并发执行的协程中获取值可以通过关键字`select`来完成,它和`switch`控制语句非常相似章节5.3)也被称作通信开关;它的行为像是“你准备好了吗”的轮询机制;`select`监听进入通道的数据,也可以是用通道发送值的时候。
```go
select {
case u:= <- ch1:
...
case v:= <- ch2:
...
...
default: // no value ready to be received
...
}
```
`default` 语句是可选的fallthrough 行为,和普通的 switch 相似,是不允许的。在任何一个 case 中执行 `break` 或者 `return`select 就结束了。
`select` 做的就是:选择处理列出的多个通信情况中的一个。
- 如果都阻塞了,会等待直到其中一个可以处理
- 如果多个可以处理,随机选择一个
- 如果没有通道操作可以处理并且写了 `default` 语句,它就会执行:`default` 永远是可运行的(这就是准备好了,可以执行)。
`select` 中使用发送操作并且有 `default` 可以确保发送不被阻塞!如果没有 `default`select 就会一直阻塞。
`select` 语句实现了一种监听模式,通常用在(无限)循环中;在某种情况下,通过 `break` 语句使循环退出。
在程序 [goroutine_select.go](examples/chapter_14/goroutine_select.go) 中有 2 个通道 `ch1``ch2`,三个协程 `pump1()``pump2()``suck()`。这是一个典型的生产者消费者模式。在无限循环中,`ch1``ch2` 通过 `pump1()``pump2()` 填充整数;`suck()` 也是在无限循环中轮询输入的,通过 `select` 语句获取 `ch1``ch2` 的整数并输出。选择哪一个 case 取决于哪一个通道收到了信息。程序在 main 执行 1 秒后结束。
示例 14.10-[goroutine_select.go](examples/chapter_14/goroutine_select.go)
```go
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go pump1(ch1)
go pump2(ch2)
go suck(ch1, ch2)
time.Sleep(1e9)
}
func pump1(ch chan int) {
for i := 0; ; i++ {
ch <- i * 2
}
}
func pump2(ch chan int) {
for i := 0; ; i++ {
ch <- i + 5
}
}
func suck(ch1, ch2 chan int) {
for {
select {
case v := <-ch1:
fmt.Printf("Received on channel 1: %d\n", v)
case v := <-ch2:
fmt.Printf("Received on channel 2: %d\n", v)
}
}
}
```
输出:
```
Received on channel 2: 5
Received on channel 2: 6
Received on channel 1: 0
Received on channel 2: 7
Received on channel 2: 8
Received on channel 2: 9
Received on channel 2: 10
Received on channel 1: 2
Received on channel 2: 11
...
Received on channel 2: 47404
Received on channel 1: 94346
Received on channel 1: 94348
```
一秒内的输出非常惊人如果我们给它计数goroutine_select2.go得到了 90000 个左右的数字。
## 练习:
练习 14.7
- a在练习 5.4 的 for_loop.go 中,有一个常见的 for 循环打印数字。在函数 `tel` 中实现一个 for 循环,用协程开始这个函数并在其中给通道发送数字。`main()` 线程从通道中获取并打印。不要使用 `time.Sleep()` 来同步:[goroutine_panic.go](exercises/chapter_14/goroutine_panic.go)
- b也许你的方案有效可能会引发运行时的 panic`throw:all goroutines are asleep-deadlock!` 为什么会这样?你如何解决这个问题?[goroutine_close.go](exercises/chapter_14/goroutine_close.go)
- c解决 a的另外一种方式使用一个额外的通道传递给协程然后在结束的时候随便放点什么进去。`main()` 线程检查是否有数据发送给了这个通道,如果有就停止:[goroutine_select.go](exercises/chapter_14/goroutine_select.go)
练习 14.8
从示例 [6.13 fibonacci.go](examples/chapter_6/fibonacci.go) 的斐波那契程序开始,制定解决方案,使斐波那契周期计算独立到协程中,并可以把结果发送给通道。
结束的时候关闭通道。`main()` 函数读取通道并打印结果:[goFibonacci.go](exercises/chapter_14/gofibonacci.go)
使用练习 [6.9 fibonacci2.go](exercises/chapter_6/fibonacci2.go) 中的算法写一个更短的 [gofibonacci2.go](exercises/chapter_14/gofibonacci2.go)
使用 `select` 语句来写,并让通道退出([gofibonacci_select.go](exercises/chapter_14/gofibonacci_select.go)
注意:当给结果计时并和 6.13 对比时,我们发现使用通道通信的性能开销有轻微削减;这个例子中的算法使用协程并非性能最好的选择;但是 [gofibonacci3](exercises/chapter_14/gofibonacci3.go) 方案使用了 2 个协程带来了 3 倍的提速。
练习 14.9
做一个随机位生成器,程序可以提供无限的随机 0 或者 1 的序列:[random_bitgen.go](exercises/chapter_14/random_bitgen.go)
练习 14.10[polar_to_cartesian.go](exercises/chapter_14/polar_to_cartesian.go)
(这是一种综合练习,使用到第 4、9、11 章和本章的内容。)写一个可交互的控制台程序,要求用户输入二位平面极坐标上的点(半径和角度(度))。计算对应的笛卡尔坐标系的点的 x 和 y 并输出。使用极坐标和笛卡尔坐标的结构体。
使用通道和协程:
- `channel1` 用来接收极坐标
- `channel2` 用来接收笛卡尔坐标
转换过程需要在协程中进行,从 channel1 中读取然后发送到 channel2。实际上做这种计算不提倡使用协程和通道但是如果运算量很大很耗时这种方案设计就非常合适了。
练习 14.11 [concurrent_pi.go](exercises/chapter_14/concurrent_pi.go) / [concurrent_pi2.go](exercises/chapter_14/concurrent_pi2.go)
使用以下序列在协程中计算 pi开启一个协程来计算公式中的每一项并将结果放入通道`main()` 函数收集并累加结果,打印出 pi 的近似值。
![](images/14.4_piseries.png?raw=true)
计算执行时间(参见第 [6.11](6.11.md) 节)
再次声明这只是为了一边练习协程的概念一边找点乐子。
如果你需要的话可使用 `math.pi` 中的 Pi而且不使用协程会运算的更快。一个急速版本使用 `GOMAXPROCS`,开启和 `GOMAXPROCS` 同样多个协程。
习惯用法:后台服务模式
服务通常是是用后台协程中的无限循环实现的,在循环中使用 `select` 获取并处理通道中的数据:
```go
// Backend goroutine.
func backend() {
for {
select {
case cmd := <-ch1:
// Handle ...
case cmd := <-ch2:
...
case cmd := <-chStop:
// stop server
}
}
}
```
在程序的其他地方给通道 `ch1``ch2` 发送数据,比如:通道 `stop` 用来清理结束服务程序。
另一种方式(但是不太灵活)就是(客户端)在 `chRequest` 上提交请求,后台协程循环这个通道,使用 `switch` 根据请求的行为来分别处理:
```go
func backend() {
for req := range chRequest {
switch req.Subjext() {
case A1: // Handle case ...
case A2: // Handle case ...
default:
// Handle illegal request ..
// ...
}
}
}
```
## 链接
- [目录](directory.md)
- 上一节:[通道的同步:关闭通道-测试阻塞的通道](14.3.md)
- 下一节:[通道超时和计时器Ticker](14.5.md)