定时器
Go语言的定时器分为两种:
- 一次性定时器(Timer):定时器值计时一次,计时结束便停止运行
- 周期性定时器(Ticker):定时器周期性的进行计时,除非主动停止,否则将永远运行
1.一次性定时器(Timer)
1.1 简介
Timer是一种单一事件的定时器,即经过指定的时间后触发一个事件,这个事件通过其本身提供的channel进行通知。 之所以叫单一事件,是因为Timer只执行一次就结束,这也是一次性定时器与周期性定时器最重要的区别。 通过timer.NewTimer(d Duration)可以创建一个Timer,参数即等待时间,时间到来后立刻触发一个事件
1.2 使用场景
1.2.1 设定超时时间
协程从管道读取数据时,如果管道内没有数据那么协程将被阻塞,一直等待管道中有数据写入;有的时候我们不希望 协程被永久阻塞,而是等待一个指定的时间,如果超过这段时间管道内仍没有数据写入,则协程可以判定为超时, 转而去处理其他逻辑。
例子如下
package main import ( "fmt" "time" ) func WaitChannel(conn <-chan string) bool{ timer:=time.NewTimer(1*time.Second) select{ case <-conn: timer.Stop() return true case <-timer.C: fmt.Println("WaitChannel timeout!") return false } } func main(){ str:=make(chan string) str1:=make(chan string,1) str1<-"abc" ok1:=WaitChannel(str1) fmt.Println(ok1) ok:=WaitChannel(str) fmt.Println(ok) } 复制代码
1.2.2 延迟执行某个方法
有的时候我们希望某个方法在今后某个时刻执行
package main import ( "fmt" "log" "time" ) func DelayFunction(){ timer:=time.NewTimer(5*time.Second) select{ case <-timer.C: log.Println("Delay 5s,Start to do sth.") } } func main() { start:=time.Now() DelayFunction() cost:=time.Since(start) fmt.Println("cost",cost," s") } 复制代码
1.3 Timer对外接口
- 创建定时器
使用func NewTimer(d Duration) *Timer方法指定一个时间即可创建一个Timer,Timer一经创建便开始计时,不需要额外的启动命令。
创建Timer意味着把一个计时任务交给系统守护协程,该协程管理着所有的Timer,
当Timer的时间到达后向Timer的管道中发送当前的时间作为事件。 - 停止定时器Timer创建后可以随时停止,停止计时器方法如下:
func(t *Timer) Stop() bool
返回值代表定时器是否超时
- true 定时器超时前停止,后续不会再发送事件
- false 定时器超时后停止
- 实际上,停止计时器意味着通知系统守护协程移除该定时器
- 重置定时器
已过期的定时器或者已经停止的定时器可以通过重置动作重新激活,重置方法如下:func (t *Timer) Reset(d Duration) bool
重置的动作实质上是先停止定时器,再启动,其返回值是停止计时器的返回值。
1.4. 简单接口
除了上面的标准接口,还提供了一些简单方法在特定情况下使用可以减少代码
- After()
- AfterFunc()
2. 周期性定时器(Ticker)
2.1 简介
Ticker是周期性定时器,即周期性的触发一个事件,通过Ticker本身提供的管道将事件传递出去。
2.2 使用场景
2.2.1 简单定时任务
有时我们希望定时执行一个任务,例如每秒记录一次日志
package main import ( "log" "time" ) func TickerDemo(){ ticker:=time.NewTicker(1*time.Second) defer ticker.Stop() for range ticker.C{ log.Println("Ticker tick.") } } func main(){ TickerDemo() }
for range语句会持续性地从管道中获取事件,收到事件后打印一行日志,如果管道中没有数据则会阻塞等待事件。由于Ticker会周期性地向管道写入事件,所以能实现周期性打印
2.2.2 定时聚合任务
有时我们希望把一些任务打包进行批量处理,例如下面的场景:
公交车发车遵循以下规则
- 公交车每隔5分钟发车,不管是否已经坐满乘客
- 已经坐满乘客的情况下,不足5分钟也会发车
package main import ( "bytes" "fmt" "math/rand" "time" ) func TickerLaunch(){ ticker:=time.NewTicker(5*time.Minute) maxPassenger:=30 Passengers:=make([]string,0,maxPassenger) for{ passenger:=GetNewPassenger(1) if passenger!=""{ Passengers=append(Passengers,passenger) }else{ time.Sleep(1*time.Second) } fmt.Println(Passengers) select { case<-ticker.C: Passengers=[]string{} default: if len(Passengers)>=maxPassenger{ Passengers=[]string{} } } fmt.Println(Passengers) } } func GetNewPassenger(codeLen int) string{ rawStr :="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_" buf := make([]byte, 0, codeLen) b := bytes.NewBuffer(buf) rand.Seed(time.Now().UnixNano()) for rawStrLen := len(rawStr);codeLen > 0; codeLen-- { randNum := rand.Intn(rawStrLen) b.WriteByte(rawStr[randNum]) } return b.String() } func main(){ TickerLaunch() } 复制代码
具体看看逻辑就好,死循环不要轻易尝试运行
3. runtimeTimer
上面的两种计时器都会在底层创建一个runtimeTimer,所以每一个版本中runtimeTimer的优化都十分重要
- Go 1.10之前:所有的runtimeTimer保存在一个全局的堆中;
- Go 1.10~1.13: runtimeTimer被拆分到多个全局堆中 ,减少了多个系统协程的锁等待时间
- Go 1.14+ : runtimeTimer保存在每个处理器P中,消除了专门的系统协程,减少了系统协程上下文切换的时间。
4. 注意事项
当我们使用Ticker的时候,如果忘记在使用结束后及时停止Ticker,就会造成资源泄露CPU使用率不断升高的情况
通常,我们在创建Ticker实例的时候就应该接着defer语句将Ticker停止
ticker:=time.NewTicker(1*time.Second) defer ticker.Stop()