1. 4.4 定时器

定时器是进程规划自己在未来某一时刻接获通知的一种机制
本节介绍两种定时器 Timer 到达指定时间触发且只触发一次 和 Ticker  间隔特定时间触发

1.1. Timer
1.1.1. 内部实现源码分析
Timer 类型 代表单次时间事件 当 Timer 到期时 当时的时间会被发送给 C (channel) 除非 Timer 是被 AfterFunc 函数创建的
注意:Timer 的实例必须通过 NewTimer 或 AfterFunc 获得

类型定义
type Timer struct {
    C <-chan Time     // The channel on which the time is delivered.
    r runtimeTimer
}

C 已经解释了 看看 runtimeTimer 它定义在 sleep.go 文件中 必须和 runtime 包中 time.go 文件中的 timer 必须保持一致
type timer struct {
    i int // heap index
    // Timer wakes up at when, and then at when+period, ... (period > 0 only)
    // each time calling f(now, arg) in the timer goroutine, so f must be
    // a well-behaved function and not block.
    when   int64
    period int64
    f      func(interface{}, uintptr)
    arg    interface{}
    seq    uintptr
}
通过 NewTimer() 来看这些字段都怎么赋值 是什么用途
// NewTimer creates a new Timer that will send
// the current time on its channel after at least duration d.
func NewTimer(d Duration) *Timer {
    c := make(chan Time, 1)
    t := &Timer{
        C: c,
        r: runtimeTimer{
            when: when(d),
            f:    sendTime,
            arg:  c,
        },
    }
    startTimer(&t.r)
    return t
}
在 when 表示的时间到时 会往 Timer.C 中发送当前时间
when 表示的时间 是 纳秒时间 正常通过 runtimeNano() + int64(d) 赋值 跟上一节中讲到的 now() 类似 runtimeNano() 也在 runtime 中实现(runtime·nanotime)

调用系统调用 clock_gettime 获取时钟值(这是 POSIX 时钟) 其中 clockid_t 时钟类型是 CLOCK_MONOTONIC 也就是不可设定的恒定态时钟 具体的是什么时间 SUSv3 规定始于未予规范的过去某一点 Linux 上 始于系统启动
如果 clock_gettime 不存在 则使用精度差些的系统调用 gettimeofday

f 参数的值是 sendTime 定时器时间到时 会调用 f 并将 arg 和 seq 传给 f
因为 Timer 是一次性的 所以 period 保留默认值 0

定时器的具体实现逻辑
都在 runtime 中的 time.go 中 它的实现 没有采用经典 Unix 间隔定时器 setitimer
系统调用 也没有 采用 POSIX 间隔式定时器(相关系统调用 timer_create timer_settime 和 timer_delete)
而是通过四叉树堆(heep)实现的(runtimeTimer 结构中的 i 字段 表示在堆中的索引)
通过构建一个最小堆 保证最快拿到到期了的定时器执行 定时器的执行
在专门的 goroutine 中进行的 go timerproc() 有兴趣的同学 可以阅读 runtime/time.go 的源码

1.1.2. Timer 相关函数或方法的使用
通过 time.After 模拟超时
c := make(chan int)
go func() {
    // time.Sleep(1 * time.Second)
    time.Sleep(3 * time.Second)
    <-c
}()

select {
case c <- 1:
    fmt.Println("channel...")
case <-time.After(2 * time.Second):
    close(c)
    fmt.Println("timeout...")
}

time.Stop 停止定时器 或 time.Reset 重置定时器

start := time.Now()
timer := time.AfterFunc(2*time.Second, func() {
    fmt.Println("after func callback, elaspe:", time.Now().Sub(start))
})

time.Sleep(1 * time.Second)
// time.Sleep(3*time.Second)
// Reset 在 Timer 还未触发时返回 true;触发了或Stop了 返回false
if timer.Reset(3 * time.Second) {
    fmt.Println("timer has not trigger!")
} else {
    fmt.Println("timer had expired or stop!")
}

time.Sleep(10 * time.Second)
// output:
// timer has not trigger!
// after func callback, elaspe: 4.00026461s
如果定时器还未触发 Stop 会将其移除 并返回 true
否则返回 false
后续再对该 Timer 调用 Stop 直接返回 false
Reset 会先调用 stopTimer 再调用 startTimer 类似于废弃之前的定时器
重新启动一个定时器 返回值和 Stop 一样

1.1.3. Sleep 的内部实现
查看 runtime/time.go 文件中的 timeSleep 可知 Sleep 的是通过 Timer 实现的
把当前 goroutine 作为 arg 参数(getg())

1.2. Ticker 相关函数或方法的使用
Ticker 和 Timer 类似
区别是 Ticker 中的 runtimeTimer 字段的 period 字段会赋值为 NewTicker(d Duration) 中的 d 表示每间隔 d 纳秒 定时器就会触发一次

除非程序终止前定时器一直需要触发
否则 不需要时应该调用 Ticker.Stop 来释放相关资源

如果程序终止前需要定时器一直触发 可以使用更简单方便的 time.Tick 函数
因为 Ticker 实例隐藏起来了 因此 该函数启动的定时器无法停止

1.3. 定时器的实际应用
在实际开发中 定时器用的较多的会是 Timer 如模拟超时 而需要类似 Tiker 的功能时
可以使用实现了 cron spec 的库 cron 感兴趣的可以参考文章 Go语言版crontab