menu arrow_back 湛蓝安全空间 |狂野湛蓝,暴躁每天 chevron_right All_wiki chevron_right the-way-to-go_ZH_CN chevron_right eBook chevron_right 14.5.md
  • home 首页
  • brightness_4 暗黑模式
  • cloud
    xLIYhHS7e34ez7Ma
    cloud
    湛蓝安全
    code
    Github
    14.5.md
    6.49 KB / 2024-07-16 23:14:29
        # 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)
    
    
    links
    file_download