The realization principle of GoLang timer

Introduction

There is often a need to execute certain code blocks regularly at work. If it is PHP code, generally write a script and then implement it with Cron.

Two timers are provided in Go: Timer(triggered at a specified time and only once) and Ticker( triggered at a specified time interval).

The implementation of Timer and Ticker is almost the same, Ticker is relatively more complicated, here is mainly about how Ticker is implemented.

Let's first look at how to use Ticker

//创建Ticker,设置多长时间触发一次
ticker := time.NewTicker(time.Second * 10)
go func() {
    
    
   for range ticker.C {
    
     //遍历ticker.C,如果有值,则会执行do someting,否则阻塞
      //do someting
   }
}()

The code is very concise and provides great convenience to developers. How does GoLang implement this function?

principle

NewTicker

NewTicker function of time/tick.go:

Ticker can be generated by calling NewTicker. There are four points to explain about this function.

  1. One of the main functions of NewTicker is initialization
  2. The time in NewTicker is in nanoseconds. When returns the nanosecond value from the current time + d, d must be a positive value
  3. The Ticker structure contains channel, sendTime is a function, and the logic is to wait for c to be assigned with select
  4. The mysterious startTimer function reveals how channel and sendTime are related
// NewTicker returns a new Ticker containing a channel that will send the
// time with a period specified by the duration argument.
// It adjusts the intervals or drops ticks to make up for slow receivers.
// The duration d must be greater than zero; if not, NewTicker will panic.
// Stop the ticker to release associated resources.
func NewTicker(d Duration) *Ticker {
    
    
   if d <= 0 {
    
    
      panic(errors.New("non-positive interval for NewTicker"))
   }
   // Give the channel a 1-element time buffer.
   // If the client falls behind while reading, we drop ticks
   // on the floor until the client catches up.
   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
}

Ticker data structure of time/tick.go

// A Ticker holds a channel that delivers `ticks' of a clock
// at intervals.
type Ticker struct {
    
    
   C <-chan Time // The channel on which the ticks are delivered.
   r runtimeTimer
}

time/sleep.go的runtimeTimer

// Interface to timers implemented in package runtime.
// Must be in sync with ../runtime/time.go:/^type timer
type runtimeTimer struct {
    
    
   tb uintptr
   i  int

   when   int64
   period int64
   f      func(interface{
    
    }, uintptr) // NOTE: must not be closure
   arg    interface{
    
    }
   seq    uintptr
}

time/sleep.go的sendTime

func sendTime(c interface{
    
    }, seq uintptr) {
    
    
   // Non-blocking send of time on c.
   // Used in NewTimer, it cannot block anyway (buffer).
   // Used in NewTicker, dropping sends on the floor is
   // the desired behavior when the reader gets behind,
   // because the sends are periodic.
   select {
    
    
   case c.(chan Time) <- Now():
   default:
   }
}

time/sleep.go的startTimer

func startTimer(*runtimeTimer)
func stopTimer(*runtimeTimer) bool

startTimer

After reading the above code, can you guess how it is achieved?

There is a mechanism to ensure that when the time is up, sendTime is called. At this time, the channel is assigned, and the position of ticker.C is called to unblock and execute the specified logic.

Let's see if GoLang is implemented like this.

When tracing the code, we found that the startTimer in the time package is just a statement. Where is the real implementation?

runtime/time.go的startTimer

Here, go's hidden skill go:linkname is used to guide the compiler to link the current (private) method or variable to the method or variable at the specified location at compile time. In addition, the structure of timer and runtimeTimer is the same, so the program runs normally.

//startTimer将new的timer对象加入timer的堆数据结构中
//startTimer adds t to the timer heap.
//go:linkname startTimer time.startTimer
func startTimer(t *timer) {
    
    
   if raceenabled {
    
    
      racerelease(unsafe.Pointer(t))
   }
   addtimer(t)
}

runtime/time.go的addtimer

func addtimer(t *timer) {
    
    
   tb := t.assignBucket()
   lock(&tb.lock)
   ok := tb.addtimerLocked(t)
   unlock(&tb.lock)
   if !ok {
    
    
      badTimer()
   }
}

runtime/time.go的addtimerLocked

// Add a timer to the heap and start or kick timerproc if the new timer is
// earlier than any of the others.
// Timers are locked.
// Returns whether all is well: false if the data structure is corrupt
// due to user-level races.
func (tb *timersBucket) addtimerLocked(t *timer) bool {
    
    
   // when must never be negative; otherwise timerproc will overflow
   // during its delta calculation and never expire other runtime timers.
   if t.when < 0 {
    
    
      t.when = 1<<63 - 1
   }
   t.i = len(tb.t)
   tb.t = append(tb.t, t)
   if !siftupTimer(tb.t, t.i) {
    
    
      return false
   }
   if t.i == 0 {
    
    
      // siftup moved to top: new earliest deadline.
      if tb.sleeping && tb.sleepUntil > t.when {
    
    
         tb.sleeping = false
         notewakeup(&tb.waitnote)
      }
      if tb.rescheduling {
    
    
         tb.rescheduling = false
         goready(tb.gp, 0)
      }
      if !tb.created {
    
    
         tb.created = true
         go timerproc(tb)
      }
   }
   return true
}

runtime/time.go的timerproc

func timerproc(tb *timersBucket) {
    
    
    tb.gp = getg()
    for {
    
    
        lock(&tb.lock)
        tb.sleeping = false
        now := nanotime()
        delta := int64(-1)
        for {
    
    
            if len(tb.t) == 0 {
    
     //无timer的情况
                delta = -1
                break
            }
            t := tb.t[0] //拿到堆顶的timer
            delta = t.when - now
            if delta > 0 {
    
     // 所有timer的时间都没有到期
                break
            }
            if t.period > 0 {
    
     // t[0] 是ticker类型,调整其到期时间并调整timer堆结构
                // leave in heap but adjust next time to fire
                t.when += t.period * (1 + -delta/t.period)
                siftdownTimer(tb.t, 0)
            } else {
    
    
                //Timer类型的定时器是单次的,所以这里需要将其从堆里面删除
                // 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 {
    
    
                    siftdownTimer(tb.t, 0)
                }
                t.i = -1 // mark as removed
            }
            f := t.f
            arg := t.arg
            seq := t.seq
            unlock(&tb.lock)
            if raceenabled {
    
    
                raceacquire(unsafe.Pointer(t))
            }
            f(arg, seq) //sendTimer被调用的位置 ---------------------------------------
            lock(&tb.lock)
        }
        if delta < 0 || faketime > 0 {
    
    
            // No timers left - put goroutine to sleep.
            tb.rescheduling = true
            goparkunlock(&tb.lock, "timer goroutine (idle)", traceEvGoBlock, 1)
            continue
        }
        // At least one timer pending. Sleep until then.
        tb.sleeping = true
        tb.sleepUntil = now + delta
        noteclear(&tb.waitnote)
        unlock(&tb.lock)
        notetsleepg(&tb.waitnote, delta)
    }
}

After tracing a circle, I finally traced to timerproc, and found the position f(arg, seq) where sendTimer was called, and we can see that channel c was passed to sendTimer.

What does the logic of the above code mean?

  1. All timers uniformly use a minimum heap structure to maintain, and compare the size according to the when (expiration time) of the timer;
  2. During the for loop, if the time of delta = t.when-now is greater than 0, then break, until there is a timer for the time to operate;
  3. The timer processing thread processes each timer from the top of the heap. For an expired timer, if its period>0, it means that the timer belongs to the Ticker type. Adjust its next expiration time and adjust its position in the heap, otherwise Remove the timer from the heap;
  4. Call the timer processing function and other related work;

to sum up

After reading this article, there is no strange knowledge and some feelings have been added. The great gods who wrote these source codes have a deep understanding of Go and a deep coding function.

In essence, GoLang implements the timer function with channel and heap. Let's mock it. The pseudo code is as follows:

func cronMock() {
    
    
   for {
    
    
      //从堆中获取时间最近的定时器
      t := getNearestTime()
      //如果时间还没到,则continue
      t.delta > 0 {
    
    
         continue
      }else{
    
    
         //时间到了,将当前的定时器再加一个钟
         t.when += t.duration
         //将堆重新排序
         siftdownTimer()
         //执行当前定时器指定的函数,即sendTimer
         t.sendTimer()
      }
   }
}

data

  1. Advanced golang (8)-hidden skills go:linkname
  2. Talking about the realization principle of Golang timer from 99.9% CPU

At last

If you like my article, you can follow my official account (Programmer Mala Tang)

My personal blog is: https://shidawuhen.github.io/

Review of previous articles:

technology

  1. HTTPS connection process
  2. Current limit realization 2
  3. Spike system
  4. Distributed system and consensus protocol
  5. Service framework and registry of microservices
  6. Beego framework usage
  7. Talking about microservices
  8. TCP performance optimization
  9. Current limit realization 1
  10. Redis implements distributed locks
  11. Golang source code bug tracking
  12. The realization principle of transaction atomicity, consistency and durability
  13. Detailed CDN request process
  14. Common caching techniques
  15. How to efficiently connect with third-party payment
  16. Gin framework concise version
  17. A brief analysis of InnoDB locks and transactions
  18. Algorithm summary

study notes

  1. Agile revolution
  2. How to exercise your memory
  3. Simple logic-after reading
  4. Hot air-after reading
  5. The Analects-Thoughts after Reading
  6. Sun Tzu's Art of War-Thoughts after reading

Thinking

  1. Project process management
  2. Some views on project management
  3. Some thoughts on product managers
  4. Thoughts on the career development of programmers
  5. Thinking about code review
  6. Markdown editor recommendation-typora

Guess you like

Origin blog.csdn.net/shida219/article/details/111239628