From 1f9cf7713b475e2251e2b0b9d836bd2a66ff9848 Mon Sep 17 00:00:00 2001 From: glight2000 <173959153@qq.com> Date: Thu, 31 Dec 2015 15:18:15 +0800 Subject: [PATCH] Update 14.5.md --- eBook/14.5.md | 122 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 118 insertions(+), 4 deletions(-) diff --git a/eBook/14.5.md b/eBook/14.5.md index b01c8a3..998cc66 100644 --- a/eBook/14.5.md +++ b/eBook/14.5.md @@ -1,4 +1,4 @@ -# 14.5 通道,超时和计时器 +# 14.5 通道,超时和断续器(Ticker) `time`包中有一些有趣的功能可以和通道组合使用。 @@ -14,7 +14,7 @@ type Ticker struct { 在协程周期性的执行一些事情(打印状态日志,输出,计算等等)的时候非常有用。 -调用`Stop()`使计时器停止,在`defer`语句中使用。这些都很好的适应`select`语句: +调用`Stop()`使断续器停止,在`defer`语句中使用。这些都很好的适应`select`语句: ```go ticker := time.NewTicker(updateInterval) defer ticker.Stop() @@ -39,10 +39,124 @@ 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 // 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`中使用以发送信号超时或停止协程的执行。以下代码,在`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是必要的,可以避免协程死锁以及确保超时的通道可以被垃圾回收。 + +第三种形式:假设程序从多个复制的数据库同时读取。只需要一个答案,需要接收首先到达的答案,`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()`来停止。 + + +在应用中缓存数据: + +应用程序中用到了来自数据库(或者常见的数据存储)的数据时,经常会把数据缓存到内存中,因为从数据库中获取数据的操作代价很高;如果数据库中的值不发生变化就没有问题。但是如果值有变化,我们需要一个机制来周期性的从数据库重新读取这些值:缓存的值就不可用(过期)了,而且我们也不希望用户看到陈旧的数据。这篇文章:`http://www.tideland.biz/CachingValues`讨论了一种方式,使用协程和断续器对象来实现。 ## 链接