Files
the-way-to-go_ZH_CN/eBook/14.5.md
Haigang Zhou f08d76efa6 第十四章上半部分 (#836)
Co-authored-by: Joe Chen <jc@unknwon.io>
2022-05-17 16:24:13 +08:00

200 lines
6.3 KiB
Markdown
Raw Permalink 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.5 通道、超时和计时器Ticker
`time` 包中有一些有趣的功能可以和通道组合使用。
其中就包含了 `time.Ticker` 结构体,这个对象以指定的时间间隔重复的向通道 `C` 发送时间值:
```go
type Ticker struct {
C <-chan Time // the channel on which the ticks are delivered.
// contains filtered or unexported fields
...
}
```
时间间隔的单位是 `ns`(纳秒,`int64`),在工厂函数 `time.NewTicker` 中以 `Duration` 类型的参数传入:`func NewTicker(dur) *Ticker`
在协程周期性的执行一些事情(打印状态日志,输出,计算等等)的时候非常有用。
调用 `Stop()` 使计时器停止,在 `defer` 语句中使用。这些都很好地适应 `select` 语句:
```go
ticker := time.NewTicker(updateInterval)
defer ticker.Stop()
...
select {
case u:= <-ch1:
...
case v:= <-ch2:
...
case <-ticker.C:
logState(status) // call some logging function logState
default: // no value ready to be received
...
}
```
`time.Tick()` 函数声明为 `Tick(d Duration) <-chan Time`,当你想返回一个通道而不必关闭它的时候这个函数非常有用:它以 `d` 为周期给返回的通道发送时间,`d` 是纳秒数。如果需要,像下边的代码一样,可以限制处理频率(函数 `client.Call()` 是一个 RPC 调用,这里暂不赘述(参见第 [15.9](15.9.md) 节)):
```go
import "time"
rate_per_sec := 10
var dur Duration = 1e9 / rate_per_sec
chRate := time.Tick(dur) // a tick every 1/10th of a second
for req := range requests {
<- chRate // rate limit our Service.Method RPC calls
go client.Call("Service.Method", req, ...)
}
```
这样只会按照指定频率处理请求:`chRate` 阻塞了更高的频率。每秒处理的频率可以根据机器负载(和/或)资源的情况而增加或减少。
问题 14.1:扩展上边的代码,思考如何承载周期请求数的暴增(提示:使用带缓冲通道和计时器对象)。
定时器 (`Timer`) 结构体看上去和计时器 (`Ticker`) 结构体的确很像(构造为 `NewTimer(d Duration)`),但是它只发送一次时间,在 `Dration d` 之后。
还有 `time.After(d)` 函数,声明如下:
```go
func After(d Duration) <-chan Time
```
`Duration d` 之后,当前时间被发到返回的通道;所以它和 `NewTimer(d).C` 是等价的;它类似 `Tick()`,但是 `After()` 只发送一次时间。下边有个很具体的示例,很好的阐明了 `select``default` 的作用:
示例 14.11[timer_goroutine.go](examples/chapter_14/timer_goroutine.go)
```go
package main
import (
"fmt"
"time"
)
func main() {
tick := time.Tick(1e8)
boom := time.After(5e8)
for {
select {
case <-tick:
fmt.Println("tick.")
case <-boom:
fmt.Println("BOOM!")
return
default:
fmt.Println(" .")
time.Sleep(5e7)
}
}
}
```
输出:
```
.
.
tick.
.
.
tick.
.
.
tick.
.
.
tick.
.
.
tick.
BOOM!
```
**习惯用法:简单超时模式**
要从通道 `ch` 中接收数据,但是最多等待 1 秒。先创建一个信号通道,然后启动一个 `lambda` 协程,协程在给通道发送数据之前是休眠的:
```go
timeout := make(chan bool, 1)
go func() {
time.Sleep(1e9) // one second
timeout <- true
}()
```
然后使用 `select` 语句接收 `ch` 或者 `timeout` 的数据:如果 `ch` 在 1 秒内没有收到数据,就选择到了 `time` 分支并放弃了 `ch` 的读取。
```go
select {
case <-ch:
// a read from ch has occured
case <-timeout:
// the read from ch has timed out
break
}
```
第二种形式:取消耗时很长的同步调用
也可以使用 `time.After()` 函数替换 `timeout-channel`。可以在 `select` 中通过 `time.After()` 发送的超时信号来停止协程的执行。以下代码,在 `timeoutNs` 纳秒后执行 `select``timeout` 分支后,执行 `client.Call` 的协程也随之结束,不会给通道 `ch` 返回值:
```go
ch := make(chan error, 1)
go func() { ch <- client.Call("Service.Method", args, &reply) } ()
select {
case resp := <-ch
// use resp and reply
case <-time.After(timeoutNs):
// call timed out
break
}
```
注意缓冲大小设置为 `1` 是必要的,可以避免协程死锁以及确保超时的通道可以被垃圾回收。此外,需要注意在有多个 `case` 符合条件时, `select``case` 的选择是伪随机的,如果上面的代码稍作修改如下,则 `select` 语句可能不会在定时器超时信号到来时立刻选中 `time.After(timeoutNs)` 对应的 `case`,因此协程可能不会严格按照定时器设置的时间结束。
```go
ch := make(chan int, 1)
go func() { for { ch <- 1 } } ()
L:
for {
select {
case <-ch:
// do something
case <-time.After(timeoutNs):
// call timed out
break L
}
}
```
第三种形式:假设程序从多个复制的数据库同时读取。只需要一个答案,需要接收首先到达的答案,`Query` 函数获取数据库的连接切片并请求。并行请求每一个数据库并返回收到的第一个响应:
```go
func Query(conns []Conn, query string) Result {
ch := make(chan Result, 1)
for _, conn := range conns {
go func(c Conn) {
select {
case ch <- c.DoQuery(query):
default:
}
}(conn)
}
return <- ch
}
```
再次声明,结果通道 `ch` 必须是带缓冲的:以保证第一个发送进来的数据有地方可以存放,确保放入的首个数据总会成功,所以第一个到达的值会被获取而与执行的顺序无关。正在执行的协程可以总是可以使用 `runtime.Goexit()` 来停止。
在应用中缓存数据:
应用程序中用到了来自数据库(或者常见的数据存储)的数据时,经常会把数据缓存到内存中,因为从数据库中获取数据的操作代价很高;如果数据库中的值不发生变化就没有问题。但是如果值有变化,我们需要一个机制来周期性的从数据库重新读取这些值:缓存的值就不可用(过期)了,而且我们也不希望用户看到陈旧的数据。
## 链接
- [目录](directory.md)
- 上一节:[使用select切换协程](14.4.md)
- 下一节:[协程和恢复recover](14.6.md)