time 包在go中很轻巧,也很好用,因此为了好奇,我决定研究一下,哈哈哈哈。开发中也经常使用到这个包,主要是思考:
- 大量使用
Timer不影响程序性能吗? - 使用
Timer需要注意的细节点? Timer的实现原理?time.Time的使用技巧和注意点。
一、Timer 和 Ticker
1、基本使用
这俩都是定时器,那么区别在哪
func main() {
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
// ticket 滴答器,它像表盘的秒针一样,每隔1s滴答一次,所以循环里无限滴答
ticket := time.NewTicker(time.Second)
for {
<-ticket.C
fmt.Println("ticker trigger", time.Now().Format("2006-01-02 15:04:05"))
}
}()
go func() {
// timer 定时器,它就是个纯粹的定时器,可以理解为一段时间后发生什么,像下面这种写法第二次直接无限阻塞下去了,因为channel没有数据了。
timer := time.NewTimer(time.Second)
for {
<-timer.C
fmt.Println("timer trigger", time.Now().Format("2006-01-02 15:04:05"))
}
}()
runtime.Gosched()
wg.Wait()
}
输出:
timer trigger 2020-05-25 23:44:39
ticker trigger 2020-05-25 23:44:40
ticker trigger 2020-05-25 23:44:42
ticker trigger 2020-05-25 23:44:44
可以看到,timer只会触发一次,而ticket会一直触发
简单的差别就是上面讲的, 那么ticket触发,那么如果我加入任务时间呢
go func() {
// ticket 滴答
ticket := time.NewTicker(time.Second)
for {
cur := <-ticket.C
// 休息2s
time.Sleep(2 * time.Second)
fmt.Printf("ticker trigger %s receive:%s\n", time.Now().Format("2006-01-02 15:04:05"),cur.Format("2006-01-02 15:04:05"))
}
}()
输出:
ticker trigger 2020-05-25 23:50:46 receive:2020-05-25 23:50:44
ticker trigger 2020-05-25 23:50:48 receive:2020-05-25 23:50:45
ticker trigger 2020-05-25 23:50:50 receive:2020-05-25 23:50:47
ticker trigger 2020-05-25 23:50:52 receive:2020-05-25 23:50:49
ticker trigger 2020-05-25 23:50:54 receive:2020-05-25 23:50:51
根据结果可以发现,recive 时间还是有出入的,因为Ticker本身是1s滴答一次,但是接受时间却是2s,这个是为啥呢?,后续解释
2、Reset 和 Stop 方法
reset : 关闭/重置/启动
func (t *Timer) Reset(d Duration) bool {
if t.r.f == nil {
panic("time: Reset called on uninitialized Timer")
}
w := when(d)
active := stopTimer(&t.r) // 关闭
t.r.when = w // 重置
startTimer(&t.r)// 启动
return active
}
stop : 关闭Timer
func (t *Timer) Stop() bool {
if t.r.f == nil {
panic("time: Stop called on uninitialized Timer")
}
return stopTimer(&t.r) // 关闭
}
那么使用中经常看到Stop和Reset , 那么问题:
- 1、Stop一定不会接受到通知了吗?,那么无期限的阻塞下去怎么办呢?
- 2、Stop后还能Reset吗?
问题一
func main() {
timer := time.NewTimer(time.Second)
start := time.Now()
time.Sleep(time.Second * 2)
fmt.Printf("Stop: %v\n", timer.Stop()) // 在timer启动2s后关闭timer,由于buffer是1个缓冲区,1s的时候就塞进去数据了,所以造成Stop后,channel还有数据,也就是会造成触发。
cur := <-timer.C
fmt.Printf("start %s,timer trigger %s receive:%s\n", start.Format("15:04:05"), time.Now().Format("15:04:05"), cur.Format("15:04:05"))
}
//Stop: false
//start 00:03:20,timer trigger 00:03:22 receive:00:03:21
那么出现这么问题如何解决呢?是的timer.Stop()返回值判断哇?,因此假如程序如下,会发生什么呢?
func main() {
timer := time.NewTimer(time.Second)
start := time.Now()
fmt.Printf("Stop: %v\n", timer.Stop()) // 在timer启动2s后关闭timer,由于buffer是1个缓冲区,1s的时候就塞进去数据了,所以造成Stop后,channel还有数据,也就是会造成触发。
time.Sleep(time.Second * 2)
/// 。。。。不变
}
// Stop: true
// fatal error: all goroutines are asleep - deadlock! // pannic,无法recover的panic,死锁。
此时我们可以发现只需要判断一下就行了,因为关闭成功返回true,也就是保证Chanel里没有数据。对于程序而言,因为如果出现上面这种代码,直接panic的,绝对接受不了的。因此需要做判读,也就是如果关闭成功了,就不去执行接受channel数据了,这个也是官方推荐的写法。
func main() {
timer := time.NewTimer(time.Second)
if !timer.Stop() { // 关闭成功,不去执行。
cur := <-timer.C
fmt.Printf("timer trigger %s receive:%s\n", time.Now().Format("15:04:05"), cur.Format("15:04:05"))
}
}
问题二
func main() {
timer := time.NewTimer(time.Second)
timer.Stop() // stop
timer.Reset(time.Second) // 重置
time.Sleep(time.Second * 2)// 等待2s,此时timer的channel接收到数据
if !timer.Stop() { // 关闭失败,因为触发过一次
cur := <-timer.C // 接受数据,触发定时器。
fmt.Printf("timer trigger %s receive:%s\n", time.Now().Format("15:04:05"), cur.Format("15:04:05"))
}
}
显然是可以重置的,那么具体为啥可以重置等等,我们继续深入研究。
3、源码分析
1、基本结构
可以get到,这俩对象本身来说并无结构差异,所以他们的实现决定与两个参数
type Timer struct {
C <-chan Time
r runtimeTimer
}
type Ticker struct {
C <-chan Time
r runtimeTimer
}
runtimeTimer 源码位置:src/runtime/time.go:18 ,这里就不过多解释为啥go可以结构转换了。
type runtimeTimer struct {
tb uintptr // the bucket the timer lives in, 哪个桶
i int // heap index ,堆内的索引
when int64 // 下一次触发的时间
period int64 // 循环周期,这里大于0就会循环触发,后续讲解
f func(interface{}, uintptr) // 这个是定时器触发执行的方法,回掉
arg interface{} // 这个是f的第一个参数
seq uintptr // 这个是f的第二个参数
}
2、初始化
Timer初始化源码:
func NewTimer(d Duration) *Timer {
c := make(chan Time, 1) // 一个缓冲区大小,原因至少第一次的时间是对的。比如我启动一个timer(1s),但是我使用channel去接受的时候已经过去2s,此时加入这个是blocking chan,此时就需要继续等待1s。
t := &Timer{
C: c,
r: runtimeTimer{
when: when(d),
f: sendTime,
arg: c,
},
}
startTimer(&t.r)
return t
}
func sendTime(c interface{}, seq uintptr) {
select {
case c.(chan Time) <- Now():
default: // default,说明这个是个无阻塞的方法。
}
}
Ticket初始化源码:
func NewTicker(d Duration) *Ticker {
if d <= 0 {
panic(errors.New("non-positive interval for NewTicker"))
}
c := make(chan Time, 1)
t := &Ticker{
C: c,
r: runtimeTimer{
when: when(d),
period: int64(d),// 多了一个period,代表周期执行。
f: sendTime, // 和timer的一样.
arg: c,
},
}
startTimer(&t.r)
return t
}
所以核心是理解:startTimer 和 stopTimer 方法。后续讲解。
3、startTimer 方法分析
源码位置:src/runtime/time.go:110
// go linkname不做解释,有兴趣的可以去了解一下。多为了系统实现,防止外部程序直接使用
//go:linkname startTimer time.startTimer
func startTimer(t *timer) {
if raceenabled { // race,说明这个方法本身存在并发安全的问题。
racerelease(unsafe.Pointer(t))
}
addtimer(t)
}
addtimer 方法
func addtimer(t *timer) {
tb := t.assignBucket() // 获取桶,根据一个当前goroutine的值去mod`GOMAXPROCS(默认64)`获取对应的桶(一个goroutine的timer全部落在一起的好处是相互隔离)
lock(&tb.lock) // lock 桶,所以这个锁是锁住整个桶
ok := tb.addtimerLocked(t)
unlock(&tb.lock)
if !ok {
badTimer()
}
}
addtimerLocked 将timer添加到桶内
func (tb *timersBucket) addtimerLocked(t *timer) bool {
if t.when < 0 { // 时间<0,直接无期限限制
t.when = 1<<63 - 1
}
t.i = len(tb.t) // timer在桶中的index
tb.t = append(tb.t, t) // timer添加到桶中
if !siftupTimer(tb.t, t.i) { //添加,堆排序(heapify)
return false
}
if t.i == 0 { // 如果它在首位,也就是min(when-now)的那个
// siftup moved to top: new earliest deadline.
if tb.sleeping && tb.sleepUntil > t.when { // g睡眠 && 最近一次发生的时间比添加的这个发生时间还要迟(说明需要唤醒)
tb.sleeping = false // 唤醒
notewakeup(&tb.waitnote) // 唤醒(这里是唤醒用户程序等待timer的g)
}
if tb.rescheduling { // 如果主g被挂起,
tb.rescheduling = false //标记未挂起
goready(tb.gp, 0) // 唤醒主g
}
if !tb.created {// 第一次初始化这个bucket的主g
tb.created = true//标记已创建
go timerproc(tb)// 主g运行,死循环
}
}
return true
}
siftupTimer Heap maintenance algorithms 方法
func siftupTimer(t []*timer, i int) bool {
if i >= len(t) { // 原长度>=添加后的长度,false,正常调用这个是不可能的。
return false
}
when := t[i].when//最后那个
tmp := t[i]//
for i > 0 { //
p := (i - 1) / 4 // parent
if when >= t[p].when {
break
}
t[i] = t[p]
t[i].i = i
i = p
}
if tmp != t[i] {
t[i] = tmp
t[i].i = i
}
return true
}
timerproc 核心方法,主函数,启动一个g去循环的执行任务
func timerproc(tb *timersBucket) {
tb.gp = getg()
for {
lock(&tb.lock) // 锁住整个桶,停止add,del
tb.sleeping = false // 停止sleep
now := nanotime() // 获取当前系统时间
delta := int64(-1)
for { // 循环处理桶内全部的timer,直到桶清空/最近的一个发生时间>now
if len(tb.t) == 0 { // 0,直接break
delta = -1
break
}
t := tb.t[0] // 第一个是时间最近的那个
delta = t.when - now
if delta > 0 { // 时间未到,break
break
}
// delta<0
ok := true
if t.period > 0 { // 循环周期>0,运行继续循环,因此需要添加到堆中,重排
// leave in heap but adjust next time to fire
t.when += t.period * (1 + -delta/t.period)
if !siftdownTimer(tb.t, 0) {
ok = false
}
} else {
// remove from heap // 从堆中移除
last := len(tb.t) - 1
if last > 0 {
tb.t[0] = tb.t[last]
tb.t[0].i = 0
}
tb.t[last] = nil
tb.t = tb.t[:last]
if last > 0 {
if !siftdownTimer(tb.t, 0) {
ok = false
}
}
t.i = -1 // mark as removed
}
// 获取f
f := t.f
arg := t.arg
seq := t.seq
unlock(&tb.lock) // 解锁
if !ok {
badTimer()
}
if raceenabled {
raceacquire(unsafe.Pointer(t))
}
f(arg, seq) // 执行func,所以这个方法不能阻塞。因此timer使用select+default,afterFunc采用开启一个新的goroutine
lock(&tb.lock) //
}
if delta < 0 || faketime > 0 { // 1、防止空转,挂起 g,与只相互对应的是goready
// No timers left - put goroutine to sleep.
tb.rescheduling = true // true表示g被挂起
goparkunlock(&tb.lock, waitReasonTimerGoroutineIdle, traceEvGoBlock, 1)
continue
}
// 如果timer时间未到,睡会觉,等待被唤醒
// At least one timer pending. Sleep until then.
tb.sleeping = true // true表示睡眠中
tb.sleepUntil = now + delta
noteclear(&tb.waitnote)
unlock(&tb.lock)
notetsleepg(&tb.waitnote, delta) //sleep delta
}
}
到此,add timer 方法就结束了,可以get到,go在这里很细节,
1、使用堆排序,排序时间,效率高
2、使用桶,解决依靠一个堆排序整个程序的任务的难度
3、当没有timer时候挂起g,防止空转的发生
4、当最近的那个发生时间还未到,挂起g,防止空转
4、stopTimer 方法分析
func stopTimer(t *timer) bool {
return deltimer(t)
}
func deltimer(t *timer) bool { // 删除timer
if t.tb == nil {
return false
}
tb := t.tb
lock(&tb.lock) // lock
removed, ok := tb.deltimerLocked(t)
unlock(&tb.lock)
if !ok {
badTimer()
}
return removed
}
deltimerLocked ,就是从堆中移除timer,
func (tb *timersBucket) deltimerLocked(t *timer) (removed, ok bool) {
// t may not be registered anymore and may have
// a bogus i (typically 0, if generated by Go).
// Verify it before proceeding.
i := t.i// index
last := len(tb.t) - 1//last
if i < 0 || i > last || tb.t[i] != t {
return false, true
}
// 被移除的不是最后那个,和最后一个交换
if i != last {
tb.t[i] = tb.t[last]
tb.t[i].i = i
}
tb.t[last] = nil
tb.t = tb.t[:last]
ok = true
if i != last {
if !siftupTimer(tb.t, i) {
ok = false
}
if !siftdownTimer(tb.t, i) {
ok = false
}
}
return true, ok
}
5、time.Sleep的实现原理
//go:linkname timeSleep time.Sleep
func timeSleep(ns int64) {
if ns <= 0 {
return
}
gp := getg() // 每一个g都有一个timer,属于g作用域,所以go的设计者它自己可以使用g这个作用域的变量,不让用户使用,类似于Java的ThreadLocal
t := gp.timer
if t == nil { // ? 这里的代码确实奇葩,唯一作用防止空指针。。
t = new(timer)
gp.timer = t
}
*t = timer{}//
t.when = nanotime() + ns//发生时间
t.f = goroutineReady // 到期限后唤醒g
t.arg = gp
tb := t.assignBucket()// 添加timer
lock(&tb.lock)
if !tb.addtimerLocked(t) {
unlock(&tb.lock)
badTimer()
}
goparkunlock(&tb.lock, waitReasonSleep, traceEvGoSleep, 2)// 挂起g
}
// 唤醒g
func goroutineReady(arg interface{}, seq uintptr) {
goready(arg.(*g), 0)
}
4、总结
1、start timer ,大致就是添加到堆中,一个g去根据触发事件执行
2、stop timer,大致就是直接从堆中移除。
3、使用堆排序的原因是什么?其次这个堆排序的原理是啥?,这个堆排序的算法叫啥?
4、最开始说的:Reset : 关闭/重置/启动,其实关闭就是从堆中移除,启动就是添加。所以本质上我们NewTimer也就是执行了添加,而Reset是移除后再添加。 Stop:关闭,本质上就是从堆中移除。所以大家懂了吗。。。。
5、日常开发中,对好使用Timer后,复用一个Timer,减少Channel的实例话,减少GC,灵活使用Reset和Stop方法达到业务效果。
6、Timer和Ticker各有使用场景,看业务需求。
7、time.Sleep 也是个Timer,调用time.Sleep的时候将当前g挂起,到触发的时候,将g的唤醒。(可以看出go的设计者复用的思想,其次就是go的开发者对于g级别的变量的偏心,只让自己用,其他人你别用,也可能是为了设计的安全,因为g的设计者认为启动/创建一个g很简单,并不希望用户给一个g绑定太多的东西,影响gc等,比如这个g回收了,那么g级别的变量就需要回收,此时造成回收难度)
5、关于Time的堆排序(动态寻找最小子节点)
1、siftupTimer
siftupTimer 从下自上过滤排序,实这个算法不难,它本质上是一个四叉堆,因为对于四叉堆来说,每一个父节点都是(index-1)/4 ,因此根据4我们确定了是四叉堆。
大致流程就是:子节点和父节点(父节点小于子节点),如果子比父还小,就交换。交换完后,父为子,继续找父的父去比较。最终找到合适的位置。
所以最差的复杂度其实是,log4(n),效率很高的。
// 这个方法参数i一直是最后一个节点
func siftupTimer(t []int, i int) bool {
if i >= len(t) {
return false
}
when := t[i]
tmp := t[i] // 最后一个节点
for i > 0 {
p := (i - 1) / 4 // 节点的父节点(四叉树)
if when >= t[p] { // 如果最后一个节点比父节点大,break
break
}
t[i] = t[p] // else交换,说明我插入的节点比较小,继续向上寻找,找到合适的位置
i = p
}
if tmp != t[i] {// 交换
t[i] = tmp
}
return true
}
2、siftdownTimer
siftdownTimer 自上而下,这种情况一般是 :根节点发生变更,需要将跟节点移除/或者值发生变化,此时一般做法是将最后一个和跟节点交换,执行siftdownTimer。算法流程很简单,就是找子节点小的交换,交换完后,子节点为目标作为父节点,继续找是否有子节点比自己小的。
所以最差的复杂度其实是,4log4(n),效率很高的。
// 这个方法参数i一直是0或者父节点
func siftdownTimer(t []int, i int) bool {
n := len(t)
if i >= n {
return false
}
when := t[i] // 根节点(目标节点)
for {
c := i*4 + 1 // left child
c3 := c + 2 // mid child
if c >= n { // 子节点 out of bound,直接break,说明遍历完毕
break
}
w := t[c]
if c+1 < n && t[c+1] < w { // 子节点1 和 子节点2比较,选最小的
w = t[c+1]
c++
}
if c3 < n {
w3 := t[c3]
if c3+1 < n && t[c3+1] < w3 { // 子节点3和4比较,选最小的。
w3 = t[c3+1]
c3++
}
if w3 < w { // 3 1比较选最小的,目的其实是为了寻找四个子节点谁最小。
w = w3
c = c3
}
}
if w >= when { // 如果 最小值比我们需要插入的节点还要大,说明我们插入的节点就是最小值,直接break
break
}
t[i] = t[c] // else,和子节点交换。继续遍历(以子节点为父节点继续找,找到子节点的子节点是否有比这个值小的)
i = c
}
if when != t[i] { // 最后把我们需要交换的节点和它交换
t[i] = when
}
return true
}
优点:我们只需要最小子节点,并不需要排序,所以利用小顶堆的特性,很好的解决了复杂度高的问题。因为对于排序算法复杂度是nlogn,但是此算法是。log4(n) 和4log4(n)。
二、Time
go的
time.Time特别的好用,简直和做加减乘除一样。。。和Java1.8加入的Time差不多,Java感觉别用Date类了吧。今天时间有限,后期再讲。。业务中经常用时间计算。