This is a creation in Article, where the information may have evolved or changed.
Design ideas
There are several main ways to implement timers under Linux:
- Implementation of timers based on linked list
- Implementation of timers based on sort list
- Timer based on minimum heap implementation
- Timer implementation based on time wheel
The time- wheel-based timer has the lowest time complexity and the most efficient, but we can implement the time-wheel timer through the priority queue .
The implementation of the priority queue can use the maximum heap and the minimum heap, so all data in the queue can define collation auto-ordering. We get the data directly from the function in the queue pop
, which is the data we want according to our custom collation.
In the Golang
implementation of a priority queue exception is simple, in the container/head
package has helped us encapsulate the implementation of the details, we just need to implement a specific interface can be.
Here is an example of what is officially provided
This example demonstrates a priority queue built using the heap interface.//an Item are something we manage in a priori Ty Queue.type Item struct {value string//The value of the item; arbitrary. priority int//The item in the queue. The index is needed by update and are maintained by the heap. Interface methods. index int//index of the item in the heap.} A Priorityqueue implements Heap. Interface and holds Items.type Priorityqueue []*itemfunc (PQ Priorityqueue) len () int {return Len (PQ)}func (PQ Priorityq Ueue) Less (i, J int) bool {//We want POPs to give us ' the highest, not lowest, priority so We use greater than here. return pq[i].priority > Pq[j].priority}func (PQ priorityqueue) Swap (i, J int) {Pq[i], pq[j] = Pq[j], Pq[i] pq[ I].index = i pq[j].index = J}func (PQ *priorityqueue) Push (x interface{}) {n: = Len (*PQ) Item: = x. (*item) it Em.index = n *PQ = append (*PQ, item)}func (PQ *priorityqueue) Pop () interface{} {old: = *pq N: = Len (old) Item: = old[n-1] Item.index =-1//For safety *PQ = old[0:n-1] ret URN Item}
Because the underlying data structure of the priority queue is constructed from a two-fork tree, we can save each node on the binary tree with an array.
Changing the array requires implementing Go
a predefined interface,,, Len
Less
Swap
Push
Pop
and update
.
Len
Interface Definition Returns queue Length
Swap
interface defines queue data precedence, comparison rules
Push
Interface definition push data to queue operations
Pop
The interface definition returns the top-level data in the queue and changes the data to delete
update
Interface definition updates the data information in the queue
Next we analyze the implementation details in Timeingwheel in the Https://github.com/leesper/tao Open source code.
First, design details
1. Structural details
1.1 Timing Task Structure
type timerType struct { id int64 expiration time.Time interval time.Duration timeout *OnTimeOut index int // for container/heap}type OnTimeOut struct { Callback func(time.Time, WriteCloser) Ctx context.Context}
Timertype structure is the abstract structure of timed tasks
id
Unique ID of the timed task, which can be used to find the scheduled task in the queue
expiration
The expiration time of the scheduled task, when the time is up, the execution of the timed task is triggered, and the priority queue is sorted by this field.
interval
Trigger frequency of timed tasks, triggered once every interval time period
timeout
This structure holds the timed timeout task, and the task function parameter must conform to the appropriate interface type
index
The subscript of the task that is saved in the queue
1.2 Time Wheel structure
type TimingWheel struct { timeOutChan chan *OnTimeOut timers timerHeapType ticker *time.Ticker wg *sync.WaitGroup addChan chan *timerType // add timer in loop cancelChan chan int64 // cancel timer in loop sizeChan chan int // get size in loop ctx context.Context cancel context.CancelFunc}
timeOutChan
Defines a cached Chan to save, timed tasks that have been triggered
timers
is a []*timerType
type of slice that saves all scheduled tasks
ticker
When each ticker arrives, the time wheel checks whether the head element in the queue reaches the time-out
wg
For concurrency control
addChan
Add a task to the queue with a cached Chan
cancelChan
The timer stopped Chan
sizeChan
Chan that returns the number of tasks in the queue
ctx
and cancel
user Concurrency control
2. Key function implementations
Main loop function of 2.1 Timingwheel
func (tw *TimingWheel) start() { for { select { case timerID := <-tw.cancelChan: index := tw.timers.getIndexByID(timerID) if index >= 0 { heap.Remove(&tw.timers, index) } case tw.sizeChan <- tw.timers.Len(): case <-tw.ctx.Done(): tw.ticker.Stop() return case timer := <-tw.addChan: heap.Push(&tw.timers, timer) case <-tw.ticker.C: timers := tw.getExpired() for _, t := range timers { tw.TimeOutChannel() <- t.timeout } tw.update(timers) } }}
The first start
function, when creating one TimeingWheel
, is performed by one, goroutine
start
in the start for loop and select to monitor the state of the different channel
<-tw.cancelChan
Returns the ID of the scheduled task to cancel and deletes it in the queue
tw.sizeChan <-
Put the number of scheduled tasks into this non-cached channel
<-tw.ctx.Done()
When the parent context executes cancel, the channel has a value indicating that it is TimeingWheel
going to stop
<-tw.addChan
Adding a task to a queue by Addchan with a cache
<-tw.ticker.C
Ticker timing, when each ticker arrives, the time packet will put the current time into the channel, and when each ticker arrives, the Timeingwheel need to check the queue to the expiration of the task ( tw.getExpired()
), with a range to put TimeOutChannel
channel, and finally in the update queue.
2.2 Timingwheel's Search timeout task function
func (tw *TimingWheel) getExpired() []*timerType { expired := make([]*timerType, 0) for tw.timers.Len() > 0 { timer := heap.Pop(&tw.timers).(*timerType) elapsed := time.Since(timer.expiration).Seconds() if elapsed > 1.0 { dylog.Warn(0, "timing_wheel", nil, "elapsed %d", elapsed) } if elapsed > 0.0 { expired = append(expired, timer) continue } else { heap.Push(&tw.timers, timer) break } } return expired}
The data is taken from the queue through a for loop until it is listed as empty or when it encounters the first task that is larger than the task start time append
expired
. Because the priority queue is expiration
sorted according to the
So when you take a task that is not on the first scheduled task, the task that indicates that the scheduled task is not up to the time.
Update queue function for 2.3 timingwheel
func (tw *TimingWheel) update(timers []*timerType) { if timers != nil { for _, t := range timers { if t.isRepeat() { // repeatable timer task t.expiration = t.expiration.Add(t.interval) // if task time out for at least 10 seconds, the expiration time needs // to be updated in case this task executes every time timer wakes up. if time.Since(t.expiration).Seconds() >= 10.0 { t.expiration = time.Now() } heap.Push(&tw.timers, t) } } }}
When the getExpired
function takes out the task to be performed in the queue, when some of the scheduled tasks need to be executed continuously, it is necessary to determine whether the scheduled task needs to be re-placed in the priority queue. isRepeat
is judged by whether the task is interval
greater than 0,
If it is greater than 0, the representation is permanent.
3. Usage of Timeingwheel
To prevent external abuse, blocking the timer association, the framework once again encapsulates the timer this package, called timer_wapper
this package, it provides two methods of invocation.
3.1 The first normal invocation of a timed task
func (t *TimerWrapper) AddTimer(when time.Time, interv time.Duration, cb TimerCallback) int64{ return t.TimingWheel.AddTimer( when, interv, serverbase.NewOnTimeOut(t.ctx, func(t time.Time, c serverbase.WriteCloser) { cb() }))}
- AddTimer Add Timer task, task in timer coprocessor execution
- When is the execution time
- Interv is the execution cycle, interv=0 executes only once
- CB as callback function
3.2 The second call to a scheduled task through a task pool
func (t *TimerWrapper) AddTimerInPool(when time.Time, interv time.Duration, cb TimerCallback) int64 { return t.TimingWheel.AddTimer( when, interv, serverbase.NewOnTimeOut(t.ctx, func(t time.Time, c serverbase.WriteCloser) { workpool.WorkerPoolInstance().Put(cb) }))}
The
parameter, like the previous parameter, only uses the task pool in the third parameter, placing the scheduled task in the task pool. The execution of a timed task itself is a put
operation.
As for put, that is workers
this package is managed. In the worker
package, which maintains a task pool, tasks in the task pool are executed in an orderly manner and are easy to manage.