文章目录
定时器
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()