# 4.4 定时器 #
定时器是进程规划自己在未来某一时刻接获通知的一种机制。本节介绍两种定时器:`Timer`(到达指定时间触发且只触发一次)和 `Ticker`(间隔特定时间触发)。
## Timer
### 内部实现源码分析
`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` 的源码。
### 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` 一样。
### Sleep 的内部实现
查看 `runtime/time.go` 文件中的 `timeSleep` 可知,`Sleep` 的是通过 `Timer` 实现的,把当前 goroutine 作为 `arg` 参数(`getg()`)。
## Ticker 相关函数或方法的使用
`Ticker` 和 `Timer` 类似,区别是:`Ticker` 中的`runtimeTimer` 字段的 `period` 字段会赋值为 `NewTicker(d Duration)` 中的 `d`,表示每间隔 `d` 纳秒,定时器就会触发一次。
除非程序终止前定时器一直需要触发,否则,不需要时应该调用 `Ticker.Stop` 来释放相关资源。
如果程序终止前需要定时器一直触发,可以使用更简单方便的 `time.Tick` 函数,因为 `Ticker` 实例隐藏起来了,因此,该函数启动的定时器无法停止。
## 定时器的实际应用
在实际开发中,定时器用的较多的会是 `Timer`,如模拟超时,而需要类似 `Tiker` 的功能时,可以使用实现了 `cron spec` 的库 [cron](https://github.com/robfig/cron),感兴趣的可以参考文章:[《Go语言版crontab》](http://blog.studygolang.com/2014/02/go_crontab/)。
# 导航 #
- 上一节:[Time类型详解](04.3.md)
- 下一节:[Unix 时间相关系统调用](04.5.md)
- 简介
- 第一章 输入输出 (Input/Output)
- 1.1 io — 基本的 IO 接口
- 1.2 ioutil — 方便的 IO 操作函数集
- 1.3 fmt — 格式化 IO
- 1.4 bufio — 缓存 IO
- 第二章 文本
- 2.1 strings — 字符串操作
- 2.2 bytes — byte slice 便利操作
- 2.3 strconv — 字符串和基本数据类型之间转换
- 2.4 regexp — 正则表达式
- 2.5 unicode — Unicode 码点、UTF-8/16 编码
- 第三章 数据结构与算法
- 3.1 sort — 排序算法
- 3.2 index/suffixarray — 后缀数组实现子字符串查询
- 3.3 container — 容器数据类型:heap、list 和 ring
- 第四章 日期与时间
- 4.1 主要类型概述
- 4.2 时区
- 4.3 Time类型详解
- 4.4 定时器
- 第六章 文件系统
- 6.1 os — 平台无关的操作系统功能实现
- 6.2 path/filepath — 操作路径
- 第七章 数据持久存储与交换
- 7.1 database/sql — SQL/SQL-Like 数据库操作接口
- 第八章 数据压缩与归档
- 8.1 flate * DEFLATE 压缩算法
- 第九章 测试
- 9.1 testing * 单元测试
- 9.2 testing * 基准测试
- 9.3 testing * 子测试
- 9.4 testing * 运行并验证示例
- 9.5 testing * 其他功能
- 9.6 httptest * HTTP 测试辅助工具
- 9.7 总结
- 第十章 进程、线程与 goroutine
- 10.1 创建进程
- 10.2 进程属性和控制
- 10.3 线程
- 10.4 进程间通信
- 第十三章 应用构建 与 debug
- 13.1 flag * 命令行参数解析
- 13.2 log * 日志记录
- 13.3 expvar * 公共变量的标准化接口
- 13.4 runtime/debug * 运行时的调试工具