01 time.timer

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// runtime/time.go

type timer struct {
    pp puintptr
    when   int64
    period int64
    f      func(interface{}, uintptr)
    arg    interface{}
    seq    uintptr
    nextwhen int64
    status uint32
}
  • timer is defined in runtime/time.go.
  • pp is a pointer to the current counter on p and a pointer to the heap.
  • when is the scale, indicating how often to trigger.
  • nextWhen indicates the nanosecond timestamp of the next trigger, the underlying cpu time of the call.
  • f is the method that needs to be executed after the trigger. arg is the argument to the method.

A timer has many states, and the state transformation of a timer can be seen in the source code.

 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
// 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

Going further down here involves the implementation of the system CPU timer.

1
2
3
4
func startTimer(*runtimeTimer)
func stopTimer(*runtimeTimer) bool
func resetTimer(*runtimeTimer, int64) bool
func modTimer(t *runtimeTimer, when, period int64, f func(interface{}, uintptr), arg interface{}, seq uintptr)

01 time.Sleep()

  • time.Sleep() also uses timer internally to implement timer blocking, it will try to get timer from the current g first, if not then create a new timer, you can think of it as an implementation of the timer singleton pattern. Because there can be at most one time.Sleep() running at a given moment in a g. After setting timer call gopark(), pass in the timer object to execute the timer and block the current goroutine.
  • Link to the time.Sleep() method via the golang-specific annotation go:linkname timeSleep time.Sleep. This linking process will be done during golang’s compilation SSA.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
//go:linkname timeSleep time.Sleep
func timeSleep(ns int64) {
    if ns <= 0 {
        return
    }

    gp := getg()
    t := gp.timer
    if t == nil {
        t = new(timer)
        gp.timer = t
    }
    t.f = goroutineReady
    t.arg = gp
    t.nextwhen = nanotime() + ns
    if t.nextwhen < 0 { // check for overflow.
        t.nextwhen = maxWhen
    }
    gopark(resetForSleep, unsafe.Pointer(t), waitReasonSleep, traceEvGoSleep, 1)
}

02 time.Ticker

A timer already satisfies the loop count, but it is not exported, so it cannot be used externally; instead, a time.Ticker is used. It wraps timer and provides some additional operations.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// time/tick.go

type Ticker struct {
	C <-chan Time // The channel on which the ticks are delivered.
	r runtimeTimer
}

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),
            f:      sendTime,
            arg:    c,
        },
    }
    startTimer(&t.r)
    return t
}

A ticker is also implemented at the bottom through a timer, except that it registers the sendTime method with the timer, which continues to reset the timer and sends a message to a pipe c after it is triggered, and is commonly used in the following way.

1
2
3
4
ticker := time.NewTicker(time.Second*1)
for range ticker.C {
    // do something
}

Next, let’s look at the logic of the sendTime() method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// It is a non-blocking method
func sendTime(c interface{}, seq uintptr) {
	select {
	case c.(chan Time) <- Now():
	default:
	}
}

// Returns the current system time
func Now() Time {
	sec, nsec, mono := now()
	mono -= startNano
	sec += unixToInternal - minWall
	if uint64(sec)>>33 != 0 {
		return Time{uint64(nsec), sec + minWall, Local}
	}
	return Time{hasMonotonic | uint64(sec)<<nsecShift | uint64(nsec), mono, Local}
}

Therefore, the main logic is to use time.timer to implement the underlying cycle timing, and then trigger sendTime() when the time of each cycle is up, and then notify the user via the channel in this method.

03 time.Timer

In addition to the commonly used time.Ticker, golang also provides time.Timer, which also maintains a timer and a channel, and its definition is the same as `time.

1
2
3
4
5
6
// time/sleep.go

type Timer struct {
	C <-chan Time
	r runtimeTimer
}

When time.NewTimer() is called.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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
}

Compared to time.NewTicker, it does not assign a value to the period field when initializing runtimeTimer. Therefore, it is one-time, while ticker is periodic.

04 The principle of the underlying timer implementation: the Time Heap

From golang’s source code we can trace the operation of its underlying timer.

1
2
3
4
func startTimer(*runtimeTimer)
func stopTimer(*runtimeTimer) bool
func resetTimer(*runtimeTimer, int64) bool
func modTimer(t *runtimeTimer, when, period int64, f func(interface{}, uintptr), arg interface{}, seq uintptr)

There is only a definition here, not an implementation because it calls the C compiler at compile time and uses its associated Linux kernel implementation. Each p has a timer array bound to it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
type p struct {
    ...
    // Actions to take at some time. This is used to implement the
    // standard library's time package.
    // Must hold timersLock to access.
    timers []*timer

    // Number of timers in P's heap.
    // Modified using atomic instructions.
    numTimers uint32

    // Number of timerDeleted timers in P's heap.
    // Modified using atomic instructions.
    deletedTimers uint32

    // Race context used while executing timer functions.
    timerRaceCtx uintptr
    ...
}

It is essentially a quadtree minimum heap, and each time runtime determines the top element of the heap by the cpu clock to see if the timestamp is less than or equal to the cpu time, and if so, the callback method corresponding to the timer registration is executed. Therefore, each heap adjustment requires nlog(n) time complexity. The next step is the operation of runtime on the timer.

 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
func addtimer(t *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")
	}
	t.status = timerWaiting

	when := t.when	
	mp := acquirem()
	pp := getg().m.p.ptr()
	lock(&pp.timersLock)
	cleantimers(pp)

	doaddtimer(pp, t)
	unlock(&pp.timersLock)

	wakeNetPoller(when)

	releasem(mp)
}

It will add him to the timer[] heap of p. Of course, this trigger will also involve the scheduling of g