]> Cypherpunks repositories - gostls13.git/commitdiff
An experimental implemenation of Ticker using two goroutines for all tickers.
authorRob Pike <r@golang.org>
Thu, 28 Jan 2010 21:34:40 +0000 (08:34 +1100)
committerRob Pike <r@golang.org>
Thu, 28 Jan 2010 21:34:40 +0000 (08:34 +1100)
Feel free to suggest other approaches.

R=rsc
CC=cw, golang-dev
https://golang.org/cl/193070

src/pkg/time/tick.go

index 98576dfe3063b65de57e21e39f3743867d9ad68c..db3f68776b678157ed72cfe1148013340349c15c 100644 (file)
 
 package time
 
-// TODO(rsc): This implementation of Tick is a
-// simple placeholder.  Eventually, there will need to be
-// a single central time server no matter how many tickers
-// are active.
-//
-// Also, if timeouts become part of the select statement,
-// perhaps the Ticker is just:
-//
-//     func Ticker(ns int64, c chan int64) {
-//             for {
-//                     select { timeout ns: }
-//                     nsec, err := Nanoseconds();
-//                     c <- nsec;
-//             }
-
+import (
+       "once"
+)
 
 // A Ticker holds a synchronous channel that delivers `ticks' of a clock
 // at intervals.
 type Ticker struct {
        C        <-chan int64 // The channel on which the ticks are delivered.
-       done     chan bool
+       c        chan<- int64 // The same channel, but the end we use.
        ns       int64
        shutdown bool
+       nextTick int64
+       next     *Ticker
 }
 
 // Stop turns off a ticker.  After Stop, no more ticks will be sent.
-func (t *Ticker) Stop() {
-       t.shutdown = true
-       go t.drain()
+func (t *Ticker) Stop() { t.shutdown = true }
+
+// Tick is a convenience wrapper for NewTicker providing access to the ticking
+// channel only.  Useful for clients that have no need to shut down the ticker.
+func Tick(ns int64) <-chan int64 {
+       if ns <= 0 {
+               return nil
+       }
+       return NewTicker(ns).C
 }
 
-func (t *Ticker) drain() {
-       for {
-               select {
-               case <-t.C:
-               case <-t.done:
-                       return
+type alarmer struct {
+       wakeUp   chan bool // wakeup signals sent/received here
+       wakeMeAt chan int64
+       wakeTime int64
+}
+
+// Set alarm to go off at time ns, if not already set earlier.
+func (a *alarmer) set(ns int64) {
+       // If there's no wakeLoop or the next tick we expect is too late, start a new wakeLoop
+       if a.wakeMeAt == nil || a.wakeTime > ns {
+               // Stop previous wakeLoop.
+               if a.wakeMeAt != nil {
+                       a.wakeMeAt <- -1
                }
+               a.wakeMeAt = make(chan int64, 10)
+               go wakeLoop(a.wakeMeAt, a.wakeUp)
+               a.wakeMeAt <- ns
        }
 }
 
-func (t *Ticker) ticker(c chan<- int64) {
-       now := Nanoseconds()
-       when := now
-       for !t.shutdown {
-               when += t.ns // next alarm
+// Channel to notify tickerLoop of new Tickers being created.
+var newTicker chan *Ticker
 
-               // if c <- now took too long, skip ahead
-               if when < now {
-                       // one big step
-                       when += (now - when) / t.ns * t.ns
-               }
-               for when <= now {
-                       // little steps until when > now
-                       when += t.ns
-               }
+func startTickerLoop() {
+       newTicker = make(chan *Ticker)
+       go tickerLoop()
+}
 
-               for !t.shutdown && when > now {
-                       // limit individual sleeps so that stopped
-                       // long-term tickers don't pile up.
-                       const maxSleep = 1e9
-                       if when-now > maxSleep {
-                               Sleep(maxSleep)
-                       } else {
-                               Sleep(when - now)
-                       }
-                       now = Nanoseconds()
+// wakeLoop delivers ticks at scheduled times, sleeping until the right moment.
+// If another, earlier Ticker is created while it sleeps, tickerLoop() will start a new
+// wakeLoop but they will share the wakeUp channel and signal that this one
+// is done by giving it a negative time request.
+func wakeLoop(wakeMeAt chan int64, wakeUp chan bool) {
+       for {
+               wakeAt := <-wakeMeAt
+               if wakeAt < 0 { // tickerLoop has started another wakeLoop
+                       return
                }
-               if t.shutdown {
-                       break
+               now := Nanoseconds()
+               if wakeAt > now {
+                       Sleep(wakeAt - now)
+                       now = Nanoseconds()
                }
-               c <- now
+               wakeUp <- true
        }
-       t.done <- true
 }
 
-// Tick is a convenience wrapper for NewTicker providing access to the ticking
-// channel only.  Useful for clients that have no need to shut down the ticker.
-func Tick(ns int64) <-chan int64 {
-       if ns <= 0 {
-               return nil
+// A single tickerLoop serves all ticks to Tickers.  It waits for two events:
+// either the creation of a new Ticker or a tick from the alarm,
+// signalling a time to wake up one or more Tickers.
+func tickerLoop() {
+       // Represents the next alarm to be delivered.
+       var alarm alarmer
+       // All wakeLoops deliver wakeups to this channel.
+       alarm.wakeUp = make(chan bool, 10)
+       var now, prevTime, wakeTime int64
+       var tickers *Ticker
+       for {
+               select {
+               case t := <-newTicker:
+                       // Add Ticker to list
+                       t.next = tickers
+                       tickers = t
+                       // Arrange for a new alarm if this one precedes the existing one.
+                       alarm.set(t.nextTick)
+               case <-alarm.wakeUp:
+                       now = Nanoseconds()
+                       // Ignore an old time due to a dying wakeLoop
+                       if now < prevTime {
+                               continue
+                       }
+                       wakeTime = now + 1e15 // very long in the future
+                       var prev *Ticker = nil
+                       // Scan list of tickers, delivering updates to those
+                       // that need it and determining the next wake time.
+                       // TODO(r): list should be sorted in time order.
+                       for t := tickers; t != nil; t = t.next {
+                               if t.shutdown {
+                                       // Ticker is done; remove it from list.
+                                       if prev == nil {
+                                               tickers = t.next
+                                       } else {
+                                               prev.next = t.next
+                                       }
+                                       continue
+                               }
+                               if tickers.nextTick <= now {
+                                       if len(t.c) == 0 {
+                                               // Only send if there's room.  We must not block.
+                                               // The channel is allocated with a one-element
+                                               // buffer, which is sufficient: if he hasn't picked
+                                               // up the last tick, no point in sending more.
+                                               t.c <- now
+                                       }
+                                       t.nextTick += t.ns
+                                       if t.nextTick <= now {
+                                               // Still behind; advance in one big step.
+                                               t.nextTick += (now - t.nextTick + t.ns) / t.ns * t.ns
+                                       }
+                                       if t.nextTick > now && t.nextTick < wakeTime {
+                                               wakeTime = t.nextTick
+                                       }
+                               }
+                               prev = t
+                       }
+                       if tickers != nil {
+                               // Please send wakeup at earliest required time.
+                               // If there are no tickers, don't bother.
+                               alarm.wakeMeAt <- wakeTime
+                       }
+               }
+               prevTime = now
        }
-       return NewTicker(ns).C
 }
 
-// Ticker returns a new Ticker containing a synchronous channel that will
+// Ticker returns a new Ticker containing a channel that will
 // send the time, in nanoseconds, every ns nanoseconds.  It adjusts the
 // intervals to make up for pauses in delivery of the ticks.
 func NewTicker(ns int64) *Ticker {
        if ns <= 0 {
                return nil
        }
-       c := make(chan int64)
-       t := &Ticker{c, make(chan bool), ns, false}
-       go t.ticker(c)
+       c := make(chan int64, 1) //  See comment on send in tickerLoop
+       t := &Ticker{c, c, ns, false, Nanoseconds() + ns, nil}
+       once.Do(startTickerLoop)
+       // must be run in background so global Tickers can be created
+       go func() { newTicker <- t }()
        return t
 }