登录
首页 >  Golang >  Go教程

GoLang中的timer定时器实现原理分析

来源:脚本之家

时间:2023-02-25 11:30:51 455浏览 收藏

本篇文章主要是结合我之前面试的各种经历和实战开发中遇到的问题解决经验整理的,希望这篇《GoLang中的timer定时器实现原理分析》对你有很大帮助!欢迎收藏,分享给更多的需要的朋友学习~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 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
}
// The Timer type represents a single event.
// When the Timer expires, the current time will be sent on C,
// unless the Timer was created by AfterFunc.
// A Timer must be created with NewTimer or AfterFunc.
type Timer struct {
    C
1
2
func NewTicker(d Duration) *Ticker {
    if d

ticker 跟 timer 的初始化过程差不多,但是 ticker 比 timer 多了一个 period 参数,意为间隔的意思。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Interface to timers implemented in package runtime.
// Must be in sync with ../runtime/time.go:/^type timer
type runtimeTimer struct {
    pp       uintptr
    when     int64 //触发时间
    period   int64 //执行周期性任务的时间间隔
    f        func(any, uintptr) // 执行的回调函数,NOTE: must not be closure
    arg      any //执行任务的参数
    seq      uintptr //回调函数的参数,该参数仅在 netpoll 的应用场景下使用
    nextwhen int64 //如果是周期性任务,下次执行任务时间
    status   uint32 //状态
}
// sendTime does a non-blocking send of the current time on c.
func sendTime(c any, seq uintptr) {
    select {
    case c.(chan Time)

sendTime 采用非阻塞的形式,意为,不管是否存在接收方,此定时器一旦到时间了就要触发掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// runtime/runtime2.go
type p struct {
    .....
    // The when field of the first entry on the timer heap.
    // This is updated using atomic functions.
    // This is 0 if the timer heap is empty.
    // 堆顶元素什么时候执行
    timer0When uint64
    // The earliest known nextwhen field of a timer with
    // timerModifiedEarlier status. Because the timer may have been
    // modified again, there need not be any timer with this value.
    // This is updated using atomic functions.
    // This is 0 if there are no timerModifiedEarlier timers.
    // 如果有timer修改为更早执行时间了,将会将执行时间更新到更早时间
    timerModifiedEarliest uint64
    // Lock for timers. We normally access the timers while running
    // on this P, but the scheduler can also do it from a different P.
    // 操作timer的互斥锁
    timersLock mutex
    // Actions to take at some time. This is used to implement the
    // standard library's time package.
    // Must hold timersLock to access.
    //该p 上的所有timer,必须加锁去操作这个字段,因为不同的p 操作这个字段会有竞争关系
    timers []*timer
    // Number of timers in P's heap.
    // Modified using atomic instructions.
    //p 堆上所有的timer数
    numTimers uint32
    // Number of timerDeleted timers in P's heap.
    // Modified using atomic instructions.
    //被标记为删除的timer,要么是我们调用stop,要么是timer 自己触发后过期导致的删除
    deletedTimers uint32
}
// runtime/time.go
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 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
    seq    uintptr
    // What to set the when field to in timerModifiedXX status.
    nextwhen int64
    // The status field holds one of the values below.
    status uint32
}
// startTimer adds t to the timer heap.
//go:linkname startTimer time.startTimer
func startTimer(t *timer) {
    if raceenabled {
        racerelease(unsafe.Pointer(t))
    }
    addtimer(t)
}
// stopTimer stops a timer.
// It reports whether t was stopped before being run.
//go:linkname stopTimer time.stopTimer
func stopTimer(t *timer) bool {
    return deltimer(t)
}
// addtimer adds a timer to the current P.
// This should only be called with a newly created timer.
// That avoids the risk of changing the when field of a timer in some P's heap,
// which could cause the heap to become unsorted.
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

timer 存储在P中的 timers []*timer成员属性上。timers看起来是一个切片,但是它是按照runtimeTimer.when这个数值排序的小顶堆四叉树,触发时间越早越排在前面。

整体来讲就是父节点一定比其子节点小,子节点之间没有任何关系和大小的要求。

关于acquiremreleasem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//go:nosplit
func acquirem() *m {
    _g_ := getg()
    _g_.m.locks++
    return _g_.m
}
//go:nosplit
func releasem(mp *m) {
    _g_ := getg()
    mp.locks--
    if mp.locks == 0 && _g_.preempt {
        // restore the preemption request in case we've cleared it in newstack
        _g_.stackguard0 = stackPreempt
    }
}

acquirem函数获取当前M,并禁止M被抢占,因为M被抢占时的判断如下

1
2
3
4
//C:\Go\src\runtime\preempt.go +287
func canPreemptM(mp *m) bool {
   return mp.locks == 0 && mp.mallocing == 0 && mp.preemptoff == "" && mp.p.ptr().status == _Prunning
}
  • 运行时没有禁止抢占(m.locks == 0
  • 运行时没有在执行内存分配(m.mallocing == 0
  • 运行时没有关闭抢占机制(m.preemptoff == ""
  • M 与 P 绑定且没有进入系统调用(p.status == _Prunning

timers的触发

1
2
3
4
5
// runtime/proc.go
func checkTimers(pp *p, now int64) (rnow, pollUntil int64, ran bool)
// runtime/time.go
func runtimer(pp *p, now int64) int64
func runOneTimer(pp *p, t *timer, now int64)

runtime/time.go文件中提供了checkTimers/runtimer/runOneTimer三个方法。checkTimers方法中,如果当前p的timers长度不为0,就不断地调用runtimers。runtimes会根据堆顶的timer的状态判断其能否执行,如果可以执行就调用runOneTimer实际执行。

触发定时器的途径有两个

  • 通过调度器在调度时进行计时器的触发,findrunnable, schedule, stealWork。
  • 通过系统监控检查并触发计时器(到期未执行),sysmon。

调度器的触发一共分两种情况,一种是在调度循环的时候调用 checkTimers 方法进行计时器的触发。另外一种是当前处理器 P 没有可执行的 Timer,且没有可执行的 G。那么按照调度模型,就会去窃取其他计时器和 G。

即使是通过每次调度器调度和窃取的时候触发,但毕竟是具有一定的随机和不确定性,因此系统监控触发依然是一个兜底保障,在 Go 语言中 runtime.sysmon 方法承担了这一个责任,存在触发计时器的逻辑,在每次进行系统监控时,都会在流程上调用 timeSleepUntil 方法去获取下一个计时器应触发的时间,以及保存该计时器已打开的计时器堆的 P。

在获取完毕后会马上检查当前是否存在 GC,若是正在 STW 则获取调度互斥锁。若发现下一个计时器的触发时间已经过去,则重新调用 timeSleepUntil 获取下一个计时器的时间和相应 P 的地址。检查 sched.sysmonlock 所花费的时间是否超过 50μs。若是,则有可能前面所获取的下一个计时器触发时间已过期,因此重新调用 timeSleepUntil 方法再次获取。如果发现超过 10ms 的时间没有进行 netpoll 网络轮询,则主动调用 netpoll 方法触发轮询。同时如果存在不可抢占的处理器 P,则调用 startm 方法来运行那些应该运行,但没有在运行的计时器。

今天关于《GoLang中的timer定时器实现原理分析》的内容就介绍到这里了,是不是学起来一目了然!想要了解更多关于golang的内容请关注golang学习网公众号!

声明:本文转载于:脚本之家 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>
评论列表