Timer、Ticker使用及其注意事项

Timer、Ticker使用及其注意事项

在刚开始学习golang语言的时候就听说Timer、Ticker的使用要尤其注意,很容易出现问题,这次就来一探究竟。

本文主要脉络:

  • 介绍定时器体系,并介绍常用使用方式和错误使用方式
  • 源码解读

timer、ticker是什么?

timer和ticker都是定时器,不同的是:

  • timer是一次性的定时器
  • ticker是循环定时器,在底层实现上是timer触发后又重新设置下一次触发时间来实现的

正确的使用姿势

Timer

对于Timer,可以通过三种函数创建:time.NewTimer​、time.AfterFunc​、time.After​。

其使用范例如下:


// FnTimer1 Timer的使用用法
func FnTimer1() {timer := time.NewTimer(time.Second * 5) //返回生成的timerfmt.Printf("timer 的启动时间为:%v\n", time.Now())expire := <-timer.C //注意这个channel是一个容量为1的channel!fmt.Printf("timer 的触发时间为:%v\n", expire)
}func FnTimer2() {ch1 := make(chan int, 1)select {case e1 := <-ch1://如果ch1通道成功读取数据,则执行该case处理语句fmt.Printf("1th case is selected. e1=%v",e1)case <- time.After(2 * time.Second): //time.After直接返回生成的timer的channel,所以一般用于超时控制fmt.Println("Timed out")}
}func FnTimer3() {_ = time.AfterFunc(time.Second*5, func() { //返回的也是timer,不过可以自己传入函数进行执行fmt.Printf("定时器触发了,触发时间为%v\n", time.Now())})fmt.Printf("timer 的启动时间为:%v\n", time.Now())time.Sleep(10 * time.Second) // 确保定时器触发
}

在底层原理上,三种不同的创建方式都是调用的time.NewTimer​,不同的是:

  • time.After​直接返回的是生成的timer的channel,而time.NewTimer​是返回生成的timer。
  • time.NewTimer​定时触发channel的原理就是定时调用一个sendTime​函数,这个函数负责向channel中发送触发时间;time.AfterFunc​就是将定时调用的sendTime​函数换成了一个自定义的函数。

补充:

  • 返回的channel的容量为1,因此是一个asynchronous的channel,即在定时器fired(触发)、stop(停止)、reset(重启)之后,仍然有可能能收到数据~

  • 垃圾回收:在go1.23之前,对于处于active(没有触发且没有显式调用Stop)的Timer,gc是无法回收的,Ticket也是同样的道理。因此在高并发的场景下需要显式搭配defer time.Stop()来解决暂时的“内存泄露”的问题。

  • 上述两点在Golang 1.23得到了解决,且可能在未来的版本中channel的容量会改为0(由asynchronous改成sync),在Go 1.23相关源码的注释部分有对应的说明。

Ticket

ticket相比于timer,其会循环触发,因此通常用于循环的干一些事情,like:

// FnTicket2 Ticker的正确的使用方法!!
func FnTicket2() {ticker := time.NewTicker(1 * time.Second)stopTicker := make(chan struct{})defer ticker.Stop()go func() {for {select {case now := <-ticker.C:// do somethingfmt.Printf("ticker 的触发时间为:%v\n", now)case <-stopTicker:fmt.Println("ticker 结束")return}}}()time.Sleep(4 * time.Second)stopTicker <- struct{}{}
}

📢注意:代码块中使用stopTicker​ + select​的方式是必须的,由于Stop函数只是修改定时器的状态,并不会主动关闭channel,因此如果直接使用循环(见​**我的github仓库**​ )会直接导致Ticker永远不退出而导致内存泄露!

补充:TIcket = 触发时自动设置下一次时间的Timer,因此上面提到的Go1.23之前的Timer存在的问题依然存在。

  • 返回的channel容量为1
  • 垃圾回收:由于Ticket会一直循环触发,因此如果不显式调用time.Stop方法的话,会永久内存泄露。

此外,对于Ticket,还有一些额外的注意事项:

  • 需要使用一个额外的channel+select的方式来正确停止Ticket。
  • Reset函数的问题,由于比较长,单独整理在golang1.23版本之前 Timer Reset方法无法正确使用1,这里说个结论 :Reset函数由于内部非原子,因此无法完美的使用,建议使用goroutine+Timer的方式替代!

使用的注意事项

由于Golang 1.23版本对定时器timer、ticker做了很大的改进,因此要分成1.23之前和1.23及其之后的版本分开考虑:

以下是1.23版本关于timer、ticker部分的修改:

  1. 未停止的定时器和不再被引用的计时器可以进行垃圾回收。在 Go 1.23 之前,未停止的定时器无法被垃圾回收,直到定时器超时,而未停止的计时器永远无法被垃圾回收。Go 1.23 的实现避免了在不使用 t.Stop​ 的程序中出现资源泄漏。
  2. 定时器通道现在是同步的(无缓冲的),这使 t.Reset​ 和 t.Stop​ 方法具有更强的保证:在其中一个方法返回后,将来从定时器通道接收到的任何值都不会观察到与旧定时器配置相对应的陈旧时间值。在 Go 1.23 之前,无法使用 t.Reset​ 避免陈旧值,而使用 t.Stop​ 避免陈旧值需要仔细使用 t.Stop​ 的返回值。Go 1.23 的实现完全消除了这种担忧。

总结一下:1.23版本改进了timer、ticker的垃圾回收和 停止、重置的相关方法(Reset、Stop)。

这也就意味着在1.23版本之前,我们在使用的时候要注意:垃圾回收和停止、重置相关方法的使用
由于Reset、Stop方法的外在表现本质上上是跟缓冲区由 有缓冲 改为 无缓冲 相关,因此如果有涉及读取缓冲区我们也需要注意相关特性。

具体来说,对于1.23之前:

  • 垃圾回收:TImer的回收只会在定时器触发(expired)或者Stop​之后;Ticker只显式触发Stop​之后才会回收;
  • Reset​、Stop​使用:对于Timer,没有完美的做法,无论怎么样Reset​和Stop​都可能存在一些问题;对于Ticker,记得使用完之后显式的Stop​;

源码解读

源码解读版本:release-branch.go1.8

运作原理

timer(ticket)的运作依赖于struct p​,源码位置:src/runtime/runtime2.go:603。

所有的计时器timer都以最小四叉堆的形式存储在struct p​的timers​字段中。并且也是交给了计时器都交由处理器的网络轮询器和调度器触发,这种方式能够充分利用本地性、减少上下文的切换开销,也是目前性能最好的实现方式。

一般来说管理定时器的结构有3种:双向链表、最小堆、时间轮(很少)。

双向链表:插入|修改时间:需要遍历去寻找插入|修改的位置,时间复杂度O(N);触发:链表头触发即可,时间复杂度O(1)。

最小堆:插入|修改时间:O(logN)的时间复杂度;触发:队头触发,但是触发之后需要调整堆以保证堆的特性,O(logN)的时间复杂度。

时间轮:插入|修改时间|触发的时间复杂度都是O(1)。但是需要额外维护时间轮的结构,其占据空间随着需要维护的未来时间长度、时间精度增加。

涉及结构体

定时器体系有两种timer:一次性触发的Timer​和循环触发的Ticker​,这两种的底层结构是相同的,触发逻辑是类似的。

毕竟Ticker​的功能包含Timer​的功能。

定时器在源码中使用的结构体是timer​,其定义为:

// Package time knows the layout of this structure.
// If this struct changes, adjust ../time/sleep.go:/runtimeTimer.
type timer struct {// If this timer is on a heap, which P's heap it is on.// puintptr rather than *p to match uintptr in the versions// of this struct defined in other packages.pp puintptr //指针,指向持有当前timer的p// Timer wakes up at when, and then at when+period, ... (period > 0 only)// each time calling f(arg, now) in the timer goroutine, so f must be// a well-behaved function and not block.//// when must be positive on an active timer.when   int64              //当前计时器被唤醒的时间;period int64              //两次被唤醒的间隔;f      func(any, uintptr) //唤醒时要执行的函数arg    any                // 计时器被唤醒时调用 f 传入的参数;seq    uintptr// What to set the when field to in timerModifiedXX status.nextwhen int64 //当定时器状态变为timerModifiedXX的时,when 字段下一次要设置的值// The status field holds one of the values below.status uint32 //计时器的状态,最重要的字段之一
}

timer​在p​中的相关字段为:

type p struct {// Lock for timers. We normally access the timers while running// on this P, but the scheduler can also do it from a different P.timersLock mutex //操作timers的时候加锁// Actions to take at some time. This is used to implement the// standard library's time package.// Must hold timersLock to access.timers []*timer //timers数组// Number of timers in P's heap.// Modified using atomic instructions.numTimers uint32 //总timers的数量// Number of timerDeleted timers in P's heap.// Modified using atomic instructions.deletedTimers uint32 //处于deleted状态的timers的数量// Race context used while executing timer functions.timerRaceCtx uintptr //竞态检测相关
}

状态机

go内部的定时器的操作是并发安全的(新建定时器、停止定时器)等,为了支持并发安全和定时器的高效调度,在源码中设计了一套关于定时器的状态机,全部的状态为:

状态解释
timerNoStatus还没有设置状态
timerWaiting定时器等待触发
timerRunningtimer正在运行
timerDeleted定时器被标记删除
timerRemoving定时器从标记删除到真正删除的中间态
timerRemoved定时器真正被删除
timerModifying正在被修改的中间状态
timerModifiedEarlier定时器被修改到了更早的触发时间
timerModifiedLater定时器被修改到了更晚的触发时间
timerMoving已经被修改正在被移动

修改定时器的状态机涉及如下所示的 7 种不同操作,它们分别承担了不同的职责:

  • runtime.addtimer​ — 向当前处理器增加新的计时器
  • runtime.deltimer​ — 将计时器标记成 timerDeleted​ 删除处理器中的计时器
  • runtime.modtimer​ — 网络轮询器会调用该函数修改计时器
  • runtime.cleantimers​ — 清除队列头中的计时器,能够提升程序创建和删除计时器的性能
  • runtime.adjusttimers​ — 调整处理器持有的计时器堆,包括移动会稍后触发的计时器、删除标记为 timerDeleted​ 的计时器
  • runtime.runtimer​ — 检查队列头中的计时器,在其准备就绪时运行该计时器

状态机的变化流程图 可以大概帮我们看出timer​的不同状态的流转情况:

@startuml[*] --> timerNoStatus : 运行时创建TimertimerNoStatus -->timerWaiting : addtimertimerWaiting -->timerModifying : deltimer、modtimertimerModifying -->timerDeleted : deltimertimerModifiedLater -->timerDeleted : deltimertimerModifiedEarlier -->timerModifying : deltimertimerDeleted -->timerRemoving : cleantimers、adjusttimers、runtimertimerRemoving -->timerRemoved : cleantimers、adjusttimers、runtimertimerModifiedEarlier --> timerMoving : cleantimers、adjusttimerstimerModifiedLater --> timerMoving : cleantimers、adjusttimerstimerMoving --> timerWaiting : cleantimerstimerWaiting --> timerRunning : runtimertimerRunning --> timerWaiting : runtimertimerRunning --> timerNoStatus : runtimerstate timerModifiedXX {state timerModifiedEarlier {}state timerModifiedLater {}
}timerModifying --> timerModifiedXX : modtimer
timerModifiedXX --> timerModifying : modtimertimerNoStatus   --> timerModifying : modtimer
timerModifying --> timerWaiting : modtimer
timerRemoved --> timerModifying : modtimer
timerDeleted    --> timerModifying : modtimertimerWaiting : 定时器等待触发
timerModifying : 定时器状态修改的中间态
timerDeleted : 定时器被📌标记删除的状态timerRemoving: 定时器从标记删除到真正被删除的中间态
timerRemoved: 定时器真正被删除timerModifiedEarlier: 定时器被修改到了更早的触发时间
timerModifiedLater : 定时器被修改到了更晚的触发时间timerMoving: 定时器在堆上的位置正在重新排序
timerRunning: timer正在运行
timerModifiedXX: 定时器在堆上的位置等待重新排序@enduml

实际上这些状态的流转都被完整的写在了golang的源码中,在后面逐个函数的讲解中也会涉及到:

// addtimer:
//   timerNoStatus   -> timerWaiting
//   anything else   -> panic: invalid value
// deltimer:
//   timerWaiting         -> timerModifying -> timerDeleted
//   timerModifiedEarlier -> timerModifying -> timerDeleted
//   timerModifiedLater   -> timerModifying -> timerDeleted
//   timerNoStatus        -> do nothing
//   timerDeleted         -> do nothing
//   timerRemoving        -> do nothing
//   timerRemoved         -> do nothing
//   timerRunning         -> wait until status changes
//   timerMoving          -> wait until status changes
//   timerModifying       -> wait until status changes
// modtimer:
//   timerWaiting    -> timerModifying -> timerModifiedXX
//   timerModifiedXX -> timerModifying -> timerModifiedYY
//   timerNoStatus   -> timerModifying -> timerWaiting
//   timerRemoved    -> timerModifying -> timerWaiting
//   timerDeleted    -> timerModifying -> timerModifiedXX
//   timerRunning    -> wait until status changes
//   timerMoving     -> wait until status changes
//   timerRemoving   -> wait until status changes
//   timerModifying  -> wait until status changes
// cleantimers (looks in P's timer heap):
//   timerDeleted    -> timerRemoving -> timerRemoved
//   timerModifiedXX -> timerMoving -> timerWaiting
// adjusttimers (looks in P's timer heap):
//   timerDeleted    -> timerRemoving -> timerRemoved
//   timerModifiedXX -> timerMoving -> timerWaiting
// runtimer (looks in P's timer heap):
//   timerNoStatus   -> panic: uninitialized timer
//   timerWaiting    -> timerWaiting or
//   timerWaiting    -> timerRunning -> timerNoStatus or
//   timerWaiting    -> timerRunning -> timerWaiting
//   timerModifying  -> wait until status changes
//   timerModifiedXX -> timerMoving -> timerWaiting
//   timerDeleted    -> timerRemoving -> timerRemoved
//   timerRunning    -> panic: concurrent runtimer calls
//   timerRemoved    -> panic: inconsistent timer heap
//   timerRemoving   -> panic: inconsistent timer heap
//   timerMoving     -> panic: inconsistent timer heap
addtimer源码

addtimer对于状态的操作:

  • timerNoStatus -> timerWaiting
  • anything else -> panic: invalid value

addtimer的主要功能:向当前p的定时器堆中添加当前定时器,并尝试唤醒网络轮训器(定时器的执行依赖于网络轮训器处理)。

这里提到的网络轮训器可能让人有点疑惑,定时器和网络有什么关系?实际上确实也没什么关系,这里提到的网络轮训器重点在于轮训器,指的更多的是select、poll、epoll那套东西。

func addtimer(t *timer) {// xxxif t.status != timerNoStatus {throw("addtimer called with initialized timer")}t.status = timerWaitingwhen := t.when// Disable preemption while using pp to avoid changing another P's heap.mp := acquirem()pp := getg().m.p.ptr()lock(&pp.timersLock)cleantimers(pp)       //尝试清除timers堆中堆头的元素,以加速定时器添加doaddtimer(pp, t)unlock(&pp.timersLock)wakeNetPoller(when)releasem(mp)
}
  • cleantimers真的能加速吗?为什么?
deltimer源码

time.stopTimer​的底层实际调用就是deltimer​。

deltimer​对于状态的操作:

// timerWaiting -> timerModifying -> timerDeleted
// timerModifiedEarlier -> timerModifying -> timerDeleted
// timerModifiedLater -> timerModifying -> timerDeleted
// timerNoStatus -> do nothing
// timerDeleted -> do nothing
// timerRemoving -> do nothing
// timerRemoved -> do nothing
// timerRunning -> wait until status changes
// timerMoving -> wait until status changes
// timerModifying -> wait until status changes

deltimer​的主要功能:对于传入的定时器进行标记删除(状态status设置为timerDeleted​)。


// deltimer deletes the timer t. It may be on some other P, so we can't
// actually remove it from the timers heap. We can only mark it as deleted.
// It will be removed in due course by the P whose heap it is on.
// Reports whether the timer was removed before it was run.
func deltimer(t *timer) bool {for {switch s := atomic.Load(&t.status); s {case timerWaiting, timerModifiedLater:// Prevent preemption while the timer is in timerModifying.// This could lead to a self-deadlock. See #38070.mp := acquirem()if atomic.Cas(&t.status, s, timerModifying) {// 必须要先拿到tpp,因为当状态设置为timerDeleted之后,timer就有可能被清除(cleantimers函数),就拿不到tpp了tpp := t.pp.ptr()if !atomic.Cas(&t.status, timerModifying, timerDeleted) {badTimer()}releasem(mp)atomic.Xadd(&tpp.deletedTimers, 1)// Timer was not yet run.return true} else {releasem(mp)}case timerModifiedEarlier:// 这里和👆🏻上面case的代码在源码中除了注释少一点其他一模一样,暂不清楚为什么mp := acquirem()if atomic.Cas(&t.status, s, timerModifying) {tpp := t.pp.ptr() //先拿到tpp,原理同上if !atomic.Cas(&t.status, timerModifying, timerDeleted) {badTimer()}releasem(mp)atomic.Xadd(&tpp.deletedTimers, 1)// Timer was not yet run.return true} else {releasem(mp)}case timerDeleted, timerRemoving, timerRemoved:// Timer was already run.return falsecase timerRunning, timerMoving:// The timer is being run or moved, by a different P.// Wait for it to complete.osyield()case timerNoStatus:// Removing timer that was never added or// has already been run. Also see issue 21874.return falsecase timerModifying:// Simultaneous calls to deltimer and modtimer.// Wait for the other call to complete.osyield()default:badTimer()}}
}
modtimer源码

time.reset​方法底层实际上调用就是modtimer​方法。

modtimer​对于状态的修改,可以简单的归纳为:

  • 当前timer还在heap中:修改为timerModifiedXX​状态(等待重新排序触发)
  • 当前timer不在heap中:修改为等待调度timerWaiting​状态
  • 当前timer在修改的中间态(XXing状态):XXing相当于是被锁定的状态,因此等待状态发生变动
//   timerWaiting    -> timerModifying -> timerModifiedXX
//   timerModifiedXX -> timerModifying -> timerModifiedYY
//   timerNoStatus   -> timerModifying -> timerWaiting
//   timerRemoved    -> timerModifying -> timerWaiting
//   timerDeleted    -> timerModifying -> timerModifiedXX
//   timerRunning    -> wait until status changes
//   timerMoving     -> wait until status changes
//   timerRemoving   -> wait until status changes
//   timerModifying  -> wait until status changes

modtimer的主要功能:重置定时器。具体来说,首先判断timer还在不在heap中

  • 还在:修改状态timerModifiedXX​,等待重新触发
  • 不在:重新添加到heap中,等待重新触发

resettimer源码

底层调用的就是modtimer,这里不多赘述了。

// resettimer resets the time when a timer should fire.
// If used for an inactive timer, the timer will become active.
// This should be called instead of addtimer if the timer value has been,
// or may have been, used previously.
// Reports whether the timer was modified before it was run.
func resettimer(t *timer, when int64) bool {return modtimer(t, when, t.period, t.f, t.arg, t.seq)
}
cleantimers源码

在addtimer中有调用,具体会在添加新定时器之前调用(addtimer源码)。

函数作用为:尝试清理heap头(第一个元素)的定时器:移除或者调整到正确的位置,可以加速addtimer​添加定时器。


// cleantimers cleans up the head of the timer queue. This speeds up
// programs that create and delete timers; leaving them in the heap
// slows down addtimer. Reports whether no timer problems were found.
// The caller must have locked the timers for pp.
func cleantimers(pp *p) {gp := getg()for {if len(pp.timers) == 0 {return}t := pp.timers[0]switch s := atomic.Load(&t.status); s {case timerDeleted: //被标记删除,现在正式移除if !atomic.Cas(&t.status, s, timerRemoving) {continue}dodeltimer0(pp)if !atomic.Cas(&t.status, timerRemoving, timerRemoved) {badTimer()}atomic.Xadd(&pp.deletedTimers, -1)case timerModifiedEarlier, timerModifiedLater: //定时器被调整,移动其到正确的位置if !atomic.Cas(&t.status, s, timerMoving) {continue}// Now we can change the when field.t.when = t.nextwhen// Move t to the right position.dodeltimer0(pp)doaddtimer(pp, t)if !atomic.Cas(&t.status, timerMoving, timerWaiting) {badTimer()}default:// Head of timers does not need adjustment.return}}
}

adjusttimers源码

adjusttimers​对状态的修改:

// adjusttimers (looks in P's timer heap):
//   timerDeleted    -> timerRemoving -> timerRemoved
//   timerModifiedXX -> timerMoving -> timerWaiting

adjusttimers​的主要作用与cleantimers​相同:尝试清理heap的定时器:移除或者调整到正确的位置。

不同的是:cleantimers只会对堆头的元素进行处理,而adjusttimers是遍历堆中所有的元素进行处理。

很有意思的一点是:对于timerModifiedXX​状态的定时器,由于是触发时间修改了,因此需要调整其在堆中的位置,golang这边选择的做法是先删除(dodeltimer​)再添加(doaddtimer​)的方法调整位置。

runtimer

对状态的修改:

// runtimer (looks in P's timer heap):
//   timerNoStatus   -> panic: uninitialized timer
//   timerWaiting    -> timerWaiting or
//   timerWaiting    -> timerRunning -> timerNoStatus or
//   timerWaiting    -> timerRunning -> timerWaiting
//   timerModifying  -> wait until status changes
//   timerModifiedXX -> timerMoving -> timerWaiting
//   timerDeleted    -> timerRemoving -> timerRemoved
//   timerRunning    -> panic: concurrent runtimer calls
//   timerRemoved    -> panic: inconsistent timer heap
//   timerRemoving   -> panic: inconsistent timer heap
//   timerMoving     -> panic: inconsistent timer heap

主要作用:循环遍历堆中第一个定时器并操作:

  • 如果第一个定时器为timerWaiting​状态:已经到达触发时间久运行并调整时间,然后返回;未到触发时间久直接返回。
  • 其它状态:进行对应的操作并再次循环。对应的操作举例:timerModifiedXX​-》调整时间;timerDeleted​-》从堆中移除定时器。

为了保证正确性,runtimer​肯定会在adjusttimers​之后运行:

if len(pp.timers) > 0 {adjusttimers(pp, now)for len(pp.timers) > 0 {// Note that runtimer may temporarily unlock// pp.timersLock.if tw := runtimer(pp, now); tw != 0 {if tw > 0 {pollUntil = tw}break}ran = true}
}
状态机规律总结
  • active​和inactive​:如果阅读了源码,会发现对于定时器的状态,还有active、inactive的分类,实际上active状态的定时器是等待未来触发的定时器(包括但不限与timeWaiting​状态),而正常不会再触发的定时器则为inactive(timeRemoved​、timerDeleted​等)。

  • 在heap中和不在heap中:下方会解释状态机的管理和堆有什么关系?

  • XXing状态相当于是一个锁定状态,不允许其他goroutine并发操作,可以理解成锁。

定时器的触发

在上面的部分中,讲解了timers体系中不同函数对于不同状态的流转。

这里将分析器的触发过程,Go 语言会在两个模块触发计时器,运行计时器中保存的函数:

  • 调度器调度时会检查处理器中的计时器是否准备就绪;
  • 系统监控会检查是否有未执行的到期计时器;

我们将依次分析上述这两个触发过程。

调度器调度

runtime.checkTimers​ 是调度器用来运行处理器中计时器的函数,它会在发生以下情况时被调用:

  • 调度器调用 runtime.schedule​ 执行调度时;
  • 调度器调用 runtime.findrunnable​ 获取可执行的 Goroutine 时;
  • 调度器调用 runtime.findrunnable​ 从其他处理器窃取计时器时;

这里不展开介绍 runtime.schedule​ 和 runtime.findrunnable​ 的实现了,重点分析用于执行计时器的runtime.checkTimers​,我们将该函数的实现分成调整计时器、运行计时器和删除计时器三个部分:

// checkTimers runs any timers for the P that are ready.
// If now is not 0 it is the current time.
// It returns the passed time or the current time if now was passed as 0.
// and the time when the next timer should run or 0 if there is no next timer,
// and reports whether it ran any timers.
// If the time when the next timer should run is not 0,
// it is always larger than the returned time.
// We pass now in and out to avoid extra calls of nanotime.
//go:yeswritebarrierrec
func checkTimers(pp *p, now int64) (rnow, pollUntil int64, ran bool) {// If it's not yet time for the first timer, or the first adjusted// timer, then there is nothing to do.next := int64(atomic.Load64(&pp.timer0When))nextAdj := int64(atomic.Load64(&pp.timerModifiedEarliest))if next == 0 || (nextAdj != 0 && nextAdj < next) {next = nextAdj}if next == 0 {// No timers to run or adjust.return now, 0, false}if now == 0 {now = nanotime()}if now < next {// Next timer is not ready to run, but keep going// if we would clear deleted timers.// This corresponds to the condition below where// we decide whether to call clearDeletedTimers.//	当前并没有到触发时间,这个检查的目的就是为了查看位于deletedTimers状态的 定时器 的比例,如果比例过大,就要清理// 	清理就是调用clearDeletedTimers函数。if pp != getg().m.p.ptr() || int(atomic.Load(&pp.deletedTimers)) <= int(atomic.Load(&pp.numTimers)/4) {return now, next, false}}lock(&pp.timersLock)if len(pp.timers) > 0 {adjusttimers(pp, now)  //上面生成的now会传下来,因此不用担心函数执行导致的时间流逝for len(pp.timers) > 0 {// Note that runtimer may temporarily unlock// pp.timersLock.if tw := runtimer(pp, now); tw != 0 { //上面生成的now会传下来,因此不用担心函数执行导致的时间流逝if tw > 0 {pollUntil = tw}break}ran = true}}// If this is the local P, and there are a lot of deleted timers,// clear them out. We only do this for the local P to reduce// lock contention on timersLock.//如果运行当前goroutine的p是持有timers数组的p 且 处于deletedTimers状态的定时器 比例超过1/4,就清理掉这部分的定时器。if pp == getg().m.p.ptr() && int(atomic.Load(&pp.deletedTimers)) > len(pp.timers)/4 {clearDeletedTimers(pp)}unlock(&pp.timersLock)return now, pollUntil, ran
}

runtime.clearDeletedTimers​ 能够避免堆中出现大量长时间运行的计时器,该函数和 runtime.moveTimers​ 也是唯二会遍历计时器堆的函数(moveTimers​ which only runs when the world is stopped)。

具体可见clearDeletedTimers​的注释:

// This is the only function that walks through the entire timer heap,
// other than moveTimers which only runs when the world is stopped.
func clearDeletedTimers(pp *p) {
系统监控

系统监控中也会触发调度器的执行,大概率是因为有时候m中可能存在不能被强占的情况,就有可能会导致timer的触发时间滞后。需要注意的是,虽然有系统监控,可以帮助timers及时触发,但是timers的触发并不能达到严格的实时性(系统监控检查timers调度延迟的阈值是10ms)。

这里我也对这个过程理解的不是很深刻,这里推荐去draveness大佬的在线图书中搜索【系统监控】关键词进行全面学习

补充问题
状态机的管理和堆有什么关系?

首先需要明确这里的堆指的是struct p​管理定时器所用的四叉堆,而不是 内存管理涉及的栈和堆。

一个定时器创建之后是timerNoStatus​状态,其并不在堆上,需要放在p​的堆上之后才能进行调度和触发!

比如如下语句中的堆字眼:

  • timerNoStatus​ 和 timerRemoved​ — 计时器不在堆上;
  • timerModifiedEarlier​ 和 timerModifiedLater​ — 计时器虽然在堆上,但是可能位于错误的位置上,需要重新排序;
// Active timers live in heaps attached to P, in the timers field.
// Inactive timers live there too temporarily, until they are removed.

衍生问题:哪些状态的timer在堆中?哪些状态在?

回答:可从源码中adjusttimers​函数中一窥究竟,timerNoStatus, timerRunning, timerRemoving, timerRemoved, timerMoving​状态的定时器是不在堆上的。

此外,有些同学可能有疑惑,定时器从堆中移除的过程(可以参考cleantimers​),是先标记成timerMoving​然后再从堆中移除,这两步不是原子的,如果状态已经是timerMoving​但是还没从堆中移除,遇上adjusttimers,岂不是会出现panic。

实际上这个问题并不会出现,因为只要涉及对p​的timers​数组操作(更改持续、加减元素)的地方都会加上锁(lock(&pp.timersLock)​),而且每个p​也只会修改自己的timers​数组,不会修改其它p​持有的timers​数组,但是同样如果不涉及数组更改,只设计状态变更的话就不需要加上锁(比如标记删除元素)。

为什么time.go和sleep.go中有接近相同的结构体?

相同的结构体示例:

time.go中:

// Package time knows the layout of this structure.
// If this struct changes, adjust ../time/sleep.go:/runtimeTimer.
// For GOOS=nacl, package syscall knows the layout of this structure.
// If this struct changes, adjust ../syscall/net_nacl.go:/runtimeTimer.
type timer struct {i int // heap index// Timer wakes up at when, and then at when+period, ... (period > 0 only)// each time calling f(arg, now) 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{}                //计时器被唤醒时调用 f 传入的参数;seq    uintptr
}

sleep.go中:

// Interface to timers implemented in package runtime.
// Must be in sync with ../runtime/time.go:/^type timer
type runtimeTimer struct {i      intwhen   int64period int64f      func(interface{}, uintptr) // NOTE: must not be closurearg    interface{}seq    uintptr
}

回答:实际上这两个结构体一个是运行时一个是编译时,不过作者目前对这块也不是特别清楚,也欢迎大家指点指点。

为什么deltimer​函数只是标记删除,并不直接删除timer?

回答:因为定时器体系中,对于timers数组的更改需要加锁,如果没有更改的话就不需要加锁,为了能快速的StopTimer,因此标记删除并不需要拿锁,效率很高。什么时候加锁可参考:struct p中的定时器加锁。

acquirem()​和releasem()​的作用

这个问题和Go的调度模型GMP息息相关,这里就做一个不严谨的解释:acquirem​ 的作用之一是 为了保证当前 P不会被切换,粗暴理解就是对P而言,其相当于不会被“打断”,从而可以保证此时修改的p是当前goroutine所属的p。

	// Disable preemption while using pp to avoid changing another P's heap.mp := acquirem()pp := getg().m.p.ptr()lock(&pp.timersLock)cleantimers(pp)doaddtimer(pp, t)unlock(&pp.timersLock)wakeNetPoller(when)releasem(mp)
定时器的状态机中为什么有这么多中间状态?

相信这篇文章读完之后,这个已经不是问题了。

nextwhen​字段的作用,其存在的必要性

首先我们要明白nextwhen​字段的作用:用于记录下一次要设置when​字段为什么值

那么既然其用于标识下一次when​字段的值,那为什么不直接修改when​字段呢?

这是因为在当前的设计中,p只会修改自己的timers数组,如果当前p​修改了其他p​的when​字段,timers数组就无法正常排序了。所以需要使用nextwhen​来记录when​需要修改的值,等timers​数组对应的p​来修改when​的值。

这里涉及跨p操作定时器的问题。

每个p都会存放在其上创建的timer​,但是不同的goroutine​可能会在不同的p​上面,因此可能操作timer​的goroutine​所在的p​与存放timer​所在的p​并不是同一个p​。

// The timer is in some other P's heap, so we can't change// the when field. If we did, the other P's heap would// be out of order. So we put the new when value in the// nextwhen field, and let the other P set the when field// when it is prepared to resort the heap.
为什么要用atomic相关变量,而不直接使用锁

猜测主要原因还是性能,锁🔐可能还是太重了。而且实际上对于有重试的代码,atomic相关的设置可能更加优雅,比如下面代码:

if !atomic.Cas(&t.status, s, timerMoving) {continue //continue用于等待重试
}

如果使用锁的话,那么伪代码如下,就算使用双重校验,可能还是很重

for {if t.status == s{ //双重校验,延迟加锁t.mu.Lock() // 锁定if t.status == s {t.status = timerMoving // 修改状态t.mu.Unlock()           // 释放锁break                    // 修改成功,退出}t.mu.Unlock() // 如果没有修改成功,解锁}// 继续等待重试}
编程风格的学习

什么时候校验值:在每一次调用的入口。虽然函数值之前已经校验过。取之:src/runtime/time.go:255

func addtimer(t *timer) {// when must be positive. A negative value will cause runtimer to// overflow during its delta calculation and never expire other runtime// timers. Zero will cause checkTimers to fail to notice the timer.if t.when <= 0 {throw("timer when must be positive")}if t.period < 0 {throw("timer period must be non-negative")}if t.status != timerNoStatus {throw("addtimer called with initialized timer")}
xxx
}

函数的分层设计

// startTimer adds t to the timer heap.
//
//go:linkname startTimer time.startTimer
func startTimer(t *timer) {if raceenabled {racerelease(unsafe.Pointer(t))}addtimer(t)
}

函数与方法:

可以思考下下面这里为什么是方法而不是p的函数。

个人认为因为这里并不设计直接修改结构体p​的值,所以设计成方法可读性更强。换言之,如果要修改对应的结构体的值,才创建函数,否则优先使用方法。


// updateTimerModifiedEarliest updates the recorded nextwhen field of the
// earlier timerModifiedEarier value.
// The timers for pp will not be locked.
func updateTimerModifiedEarliest(pp *p, nextwhen int64) {for {old := atomic.Load64(&pp.timerModifiedEarliest)if old != 0 && int64(old) < nextwhen {return}if atomic.Cas64(&pp.timerModifiedEarliest, old, uint64(nextwhen)) {return}}
}

可以用注释表明当前函数必须被什么锁给锁定住

// doaddtimer adds t to the current P's heap.
// The caller must have locked the timers for pp.   //注释说明,这个函数必须锁定之后才能进入
func doaddtimer(pp *p, t *timer) {xxx
}

总结

本文解开了Timer体系相关的状态流转,但是对于现在Timer中存在的问题(reset、垃圾回收)在golang1.23怎么得到解决的机制还没有探究,这个可以等待后续研究研究。

参考资料:

https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-timer/

计时器 | Go 语言进阶之旅

Go中定时器实现原理及源码解析-腾讯云开发者社区-腾讯云

Golang 定时器(Timer 和 Ticker ),这篇文章就够了定时器是什么Golang原生time包下可以用来执 - 掘金

golang源码

在这里插入图片描述

在这里插入图片描述

‍### golang1.23 之前到底应该如何正确的使用 `Reset`​这里给出一个使用Timer代替Ticket的方法,一般来说:每次一个新的局部变量 `Timer`​ 结构体没压力,非要复用使用 Reset 的可读性太差了,对维护者不友好,而且习惯了不好的写法,哪天一不小心就写出问题了~```go
go func() {for {func() {timer := time.NewTimer(time.Second * 2)defer timer.Stop()select {case b := <-c:if !b {fmt.Println(time.Now(), "work...")}case <-timer.C: // BBB: normal receive from channel timeout eventfmt.Println(time.Now(), "timeout")}}()}}()
```
‍参考:[https://tonybai.com/2016/12/21/how-to-use-timer-reset-in-golang-correctly/](https://tonybai.com/2016/12/21/how-to-use-timer-reset-in-golang-correctly/)[https://www.v2ex.com/t/794283](https://www.v2ex.com/t/794283)‍‍

  1. golang1.23版本之前 Timer Reset方法无法正确使用

    golang1.23 之前 Reset ​到底有什么问题

    在 golang 的 time.Reset 文档中有这么一句话,为了防止文档更新而导致内容变动,这里粘贴出来:

    Before Go 1.23, the only safe way to use Reset was to [Stop] and explicitly drain the timer first. See the NewTimer documentation for more details.
    在Go 1.23 之前,唯一安全使用Reset函数的方式是:在使用之前调用Stop函数并且明确的从timer的channel中抽取出东西。
    

    虽然文档中已经告诉了正确使用的方式,但是实际上在真正的代码中无法达到这个要求,参考下方代码(来源代码来源):

    //consumergo func() {// try to read from channel, block at most 5s.// if timeout, print time event and go on loop.// if read a message which is not the type we want(we want true, not false),// retry to read.timer := time.NewTimer(time.Second * 5)for {// timer may be not active, and firedif !timer.Stop() {select {case <-timer.C: //try to drain from the channel,尝试抽取,由于使用select,因此这里可以保证:不阻塞 + 一定抽取成功default:}}timer.Reset(time.Second * 5)  //重置定时器select {case b := <-c:if b == false {fmt.Println(time.Now(), ":recv false. continue")continue}//we want true, not falsefmt.Println(time.Now(), ":recv true. return")returncase <-timer.C:fmt.Println(time.Now(), ":timer expired")continue}}}()
    

    在上面的代码中,我们按照文档的要求,在 timer.Reset ​之前已经调用了 Stop ​函数,且如果 Stop 成功(返回 true),还尝试抽取 timer,看起来似乎没问题的代码中仍然存在问题。

    问题的关键在于:当 Ticket 触发的时候,设置定时器状态的操作和发送 channel 的操作并不是原子的,见 runOneTimer 函数。

    异常情况:尝试抽取 channel 在 发送 channel 之前,这样会导致 Reset 之前并没有真正的清空 timer,后一次的 timer.C 还没到触发时间就异常的触发了! ↩︎

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/503015.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++11——2:可变模板参数

一.前言 C11引入了可变模板参数&#xff08;variadic template parameters&#xff09;的概念&#xff0c;它允许我们在模板定义中使用可变数量的参数。这样&#xff0c;我们就可以处理任意数量的参数&#xff0c;而不仅限于固定数量的参数。 二.可变模板参数 我们早在C语言…

君正T41交叉编译ffmpeg、opencv并做h264软解,利用君正SDK做h264硬件编码

目录 1 交叉编译ffmpeg----错误解决过程&#xff0c;不要看 1.1 下载源码 1.2 配置 1.3 编译 安装 1.3.1 报错&#xff1a;libavfilter/libavfilter.so: undefined reference to fminf 1.3.2 报错&#xff1a;error: unknown type name HEVCContext; did you mean HEVCPr…

感知器的那些事

感知器的那些事 历史背景Rosenblatt和Minsky关于感知机的争论弗兰克罗森布拉特简介提出感知器算法Mark I感知机争议与分歧马文明斯基简介单层感知器工作原理训练过程多层感知器工作原理单层感知机 vs 多层感知机感知器模型(Perceptron),是由心理学家Frank Rosenblatt在1957年…

C语言:枚举类型

一、枚举类型的声明 枚举顾名思义就是一一列举。我们可以把可能的取值一一列举。比如我们现实生活中&#xff1a; 星期一到星期日是有限的7天&#xff0c;可以一一列举 &#xff1b;性别有&#xff1a;男、女、保密&#xff0c;也可以一一列举 &#xff1b;月份有12个月&#x…

25/1/6 算法笔记<强化学习> 初玩V-REP

我们安装V-REP之后&#xff0c;使用的是下面Git克隆的项目。 git clone https://github.com/deep-reinforcement-learning_book/Chapter16-Robot-Learning-in-Simulation.git 项目中直接组装好了一个机械臂。 我们先来分析下它的对象树 DefaultCamera:摄像机&#xff0c;用于…

CODESYS MODBUS TCP通信(AM400PLC作为主站通信)

禾川Q1 PLC MODBUS-TCP通信 禾川Q1 PLC MODBUS-TCP通信(CODESYS平台完整配置+代码)-CSDN博客文章浏览阅读17次。MATLAB和S7-1200PLC水箱液位高度PID控制联合仿真(MODBUSTCP通信)_将matlab仿真导入plc-CSDN博客文章浏览阅读722次。本文详细介绍了如何使用MATLAB与S7-1200PLC进行…

OSPF - 影响OSPF邻居建立的因素

总结为这么10种 routerID 冲突区域id不一致认证MA网络掩码需一致区域类型(特殊区域)hello、dead时间MTU(如果开启检查)静默接口网络类型不匹配MA网络中路由器接口优先级全为0 如何建立邻居可以查看上一篇文章&#xff0c;可以直接专栏找&#xff08;&#x1f92b;挂链接会没流…

【大数据】(选修)实验4 安装熟悉HBase数据库并实践

实验4 安装熟悉HBase数据库并实践 1、实验目的 (1)理解HBase在Hadoop体系结构中的角色; (2)熟练使用HBase操作常用的Shell命令; (3)熟悉HBase操作常用的Java API。 2、实验平台 操作系统:Linux Hadoop版本:2.6.0或以上版本 HBase版本:1.1.2或以上版本 JDK版…

windeployqt.exe打包qt程序总结(MSVC)

文章目录 前言打包步骤问题 前言 打包环境&#xff1a;windows10VS2017QT5.12.12 参考&#xff1a;Qt 打包发布程序&#xff0c;解决找不到msvcp140.dll等动态库问题正确方案 打包步骤 运行Qt5.12.12&#xff08;MSVC 2017 64-bits&#xff09; 在开始软件菜单里找到Qt文件夹…

算法的学习笔记—不用常规控制语句求 1 到 n 的和

&#x1f600;前言 在算法编程中&#xff0c;有时我们会遇到一些特殊的限制条件&#xff0c;这些限制会迫使我们跳出常规思维。本文讨论的问题就是一个典型案例&#xff1a;在不能使用基本控制语句的情况下&#xff0c;如何求解 1 到 n 的和。这个问题不仅考验编程技巧&#xf…

计算机网络 (27)IP多播

前言 IP多播&#xff08;也称多址广播或组播&#xff09;技术是一种允许一台或多台主机&#xff08;多播源&#xff09;发送单一数据包到多台主机&#xff08;一次性的、同时的&#xff09;的TCP/IP网络技术。 一、基本概念 定义&#xff1a;多播作为一点对多点的通信&#xff…

CSS 学习之正确看待 CSS 世界里的 margin 合并

一、什么是 margin 合并 块级元素的上外边距(margin-top)与下外边距(margin-bottom)有时会合并为单个外边距&#xff0c;这样的现象称为“margin 合并”。从此定义上&#xff0c;我们可以捕获两点重要的信息。 块级元素&#xff0c;但不包括浮动和绝对定位元素&#xff0c;尽…

小程序组件 —— 28 组件案例 - 推荐商品区域 - 实现结构样式

这一节目标是实现底部推荐商品的结构和样式&#xff0c;由于这里要求横向滚动&#xff0c;所以需要使用上节介绍的 scroll-view 功能&#xff0c;并使用 scroll-x 属性支持横向滚动&#xff0c;推荐商品区域中的每一个商品是一个单独的 view&#xff0c;每个view 中需要写三个组…

单片机-LED点阵实验

要将第一个点点亮&#xff0c;则 1 脚接高电平 a 脚接低电平&#xff0c;则第一个点就亮了&#xff1b;如果要将第一行点亮&#xff0c;则第 1 脚要接高电平&#xff0c;而&#xff08;a、b、c、d、e、f、g、h &#xff09;这些引脚接低电平&#xff0c;那么第一行就会点亮&…

软件项目体系建设文档,项目开发实施运维,审计,安全体系建设,验收交付,售前资料(word原件)

软件系统实施标准化流程设计至关重要&#xff0c;因为它能确保开发、测试、部署及维护等各阶段高效有序进行。标准化流程能减少人为错误&#xff0c;提升代码质量和系统稳定性。同时&#xff0c;它促进了团队成员间的沟通与协作&#xff0c;确保项目按时交付。此外&#xff0c;…

Java基础 注解

分类 Java自带的标准注解&#xff0c;包括Override、Deprecated和SuppressWarnings&#xff0c;分别用于标明重写某个方法、标明某个类或方法过时、标明要忽略的警告&#xff0c;用这些注解标明后编译器就会进行检查。元注解&#xff0c;元注解是用于定义注解的注解&#xff0…

Linux中rsync命令使用

一、rsync简介 rsync 是一种高效的文件复制和同步工具&#xff0c;常用于在本地或远程计算机之间同步文件和目录 主要特性增量同步&#xff1a;rsync 会检测源和目标文件之间的差异&#xff0c;只传输发生变化的部分&#xff0c;而不是重新传输整个文件。这样就能有效减少数据…

基于STM32的自动水满报警系统设计

目录 引言系统设计 硬件设计软件设计系统功能模块 水位检测模块报警模块自动控制模块控制算法 水位检测逻辑报警触发逻辑代码实现 水位检测模块报警控制模块自动控制逻辑系统调试与优化结论与展望 1. 引言 水满报警系统在家庭、农业、工业等领域广泛应用&#xff0c;通过实时…

【Java数据结构】二叉树

1.树型结构 1.1树的概念 树是一种非线性的数据结构&#xff0c;由n个结点组成的具有层次关系的集合。下面是它的特点&#xff1a; 根结点是没有前驱的结点&#xff08;没有父结点的结点&#xff09;子结点之间互不相交除了根结点外&#xff0c;其它结点都只有一个父结点n个结…

学习threejs,导入AWD格式的模型

&#x1f468;‍⚕️ 主页&#xff1a; gis分享者 &#x1f468;‍⚕️ 感谢各位大佬 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍⚕️ 收录于专栏&#xff1a;threejs gis工程师 文章目录 一、&#x1f340;前言1.1 ☘️THREE.AWDLoader AWD模型加…