├── .gitignore ├── README.md ├── example ├── boom │ └── boom.go └── simple │ └── simple.go ├── go.mod ├── go.sum ├── task_pool.go ├── timer.go ├── timer_test.go ├── timewheel.go └── timewheel_pool.go /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-timewheel 2 | 3 | golang timewheel lib, similar to golang std timer 4 | 5 | ## Usage 6 | 7 | ### base method 8 | 9 | init timewheel 10 | 11 | ``` 12 | tw, err := NewTimeWheel(1 * time.Second, 360) 13 | if err != nil { 14 | panic(err) 15 | } 16 | 17 | tw.Start() 18 | tw.Stop() 19 | ``` 20 | 21 | safe ticker 22 | 23 | ``` 24 | tw, _ := NewTimeWheel(1 * time.Second, 360, TickSafeMode()) 25 | ``` 26 | 27 | use sync.Pool 28 | 29 | ``` 30 | tw, _ := NewTimeWheel(1 * time.Second, 360, SetSyncPool(true)) 31 | ``` 32 | 33 | add delay task 34 | 35 | ``` 36 | task := tw.Add(5 * time.Second, func(){}) 37 | ``` 38 | 39 | remove delay task 40 | 41 | ``` 42 | tw.Remove(task) 43 | ``` 44 | 45 | add cron delay task 46 | 47 | ``` 48 | task := tw.AddCron(5 * time.Second, func(){ 49 | ... 50 | }) 51 | ``` 52 | 53 | ### similar to std time 54 | 55 | similar to time.Sleep 56 | 57 | ``` 58 | tw.Sleep(5 * time.Second) 59 | ``` 60 | 61 | similar to time.After() 62 | 63 | ``` 64 | <- tw.After(5 * time.Second) 65 | ``` 66 | 67 | similar to time.NewTimer 68 | 69 | ``` 70 | timer :=tw.NewTimer(5 * time.Second) 71 | <- timer.C 72 | timer.Reset(1 * time.Second) 73 | timer.Stop() 74 | ``` 75 | 76 | similar to time.NewTicker 77 | 78 | ``` 79 | timer :=tw.NewTicker(5 * time.Second) 80 | <- timer.C 81 | timer.Stop() 82 | ``` 83 | 84 | similar to time.AfterFunc 85 | 86 | ``` 87 | runner :=tw.AfterFunc(5 * time.Second, func(){}) 88 | <- runner.C 89 | runner.Stop() 90 | ``` 91 | 92 | ## benchmark test 93 | 94 | [example/main.go](example/main.go) 95 | -------------------------------------------------------------------------------- /example/boom/boom.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "sync/atomic" 7 | "time" 8 | 9 | "github.com/rfyiamcool/go-timewheel" 10 | ) 11 | 12 | var ( 13 | counter int64 = 0 14 | loopNum = 50 15 | tw = newTimeWheel() 16 | wg = sync.WaitGroup{} 17 | log = new(logger) 18 | ) 19 | 20 | func incrCounter() { 21 | atomic.AddInt64(&counter, 1) 22 | } 23 | 24 | func main() { 25 | go printCounter() 26 | 27 | batchRun5s() 28 | batchRun6s() 29 | batchRun8s() 30 | batchRun10s() 31 | batchRun15s() 32 | batchRun20s() 33 | batchRun30s() 34 | batchRun60s() 35 | batchRun120s() 36 | batchRun360s() 37 | 38 | log.info("add finish") 39 | wg.Wait() 40 | } 41 | 42 | func batchRun5s() { 43 | worker := 10000 44 | delay := 5 45 | beforeDiff := 1 46 | afterDiff := 2 47 | processTimer(worker, delay, beforeDiff, afterDiff) 48 | 49 | loop := 500 50 | taskNum := 50000 51 | go processCallbackLoop(loop, taskNum, delay, beforeDiff, afterDiff) 52 | } 53 | 54 | func batchRun6s() { 55 | worker := 5000 56 | delay := 6 57 | beforeDiff := 1 58 | afterDiff := 2 59 | processTimer(worker, delay, beforeDiff, afterDiff) 60 | 61 | loop := 500 62 | taskNum := 50000 63 | go processCallbackLoop(loop, taskNum, delay, beforeDiff, afterDiff) 64 | } 65 | func batchRun8s() { 66 | worker := 5000 67 | delay := 8 68 | beforeDiff := 1 69 | afterDiff := 2 70 | processTimer(worker, delay, beforeDiff, afterDiff) 71 | 72 | loop := 500 73 | taskNum := 50000 74 | go processCallbackLoop(loop, taskNum, delay, beforeDiff, afterDiff) 75 | } 76 | 77 | func batchRun10s() { 78 | worker := 10000 79 | delay := 10 80 | beforeDiff := 1 81 | afterDiff := 2 82 | processTimer(worker, delay, beforeDiff, afterDiff) 83 | 84 | loop := 300 85 | taskNum := 50000 86 | go processCallbackLoop(loop, taskNum, delay, beforeDiff, afterDiff) 87 | } 88 | 89 | func batchRun15s() { 90 | worker := 10000 91 | delay := 15 92 | beforeDiff := 1 93 | afterDiff := 2 94 | processTimer(worker, delay, beforeDiff, afterDiff) 95 | 96 | loop := 200 97 | taskNum := 50000 98 | go processCallbackLoop(loop, taskNum, delay, beforeDiff, afterDiff) 99 | } 100 | 101 | func batchRun20s() { 102 | worker := 20000 103 | delay := 20 104 | beforeDiff := 1 105 | afterDiff := 2 106 | processTimer(worker, delay, beforeDiff, afterDiff) 107 | 108 | loop := 150 109 | taskNum := 30000 110 | go processCallbackLoop(loop, taskNum, delay, beforeDiff, afterDiff) 111 | } 112 | 113 | func batchRun30s() { 114 | worker := 5000 115 | delay := 30 116 | beforeDiff := 1 117 | afterDiff := 2 118 | processTimer(worker, delay, beforeDiff, afterDiff) 119 | 120 | loop := 100 121 | taskNum := 30000 122 | go processCallbackLoop(loop, taskNum, delay, beforeDiff, afterDiff) 123 | } 124 | 125 | func batchRun60s() { 126 | worker := 10000 127 | delay := 60 128 | beforeDiff := 1 129 | afterDiff := 2 130 | processTimer(worker, delay, beforeDiff, afterDiff) 131 | 132 | loop := 50 133 | taskNum := 30000 134 | go processCallbackLoop(loop, taskNum, delay, beforeDiff, afterDiff) 135 | } 136 | 137 | func batchRun120s() { 138 | worker := 20000 139 | delay := 120 140 | beforeDiff := 1 141 | afterDiff := 2 142 | processTimer(worker, delay, beforeDiff, afterDiff) 143 | 144 | loop := 50 145 | taskNum := 30000 146 | go processCallbackLoop(loop, taskNum, delay, beforeDiff, afterDiff) 147 | } 148 | 149 | func batchRun360s() { 150 | worker := 20000 151 | delay := 360 152 | beforeDiff := 1 153 | afterDiff := 2 154 | processTimer(worker, delay, beforeDiff, afterDiff) 155 | 156 | loop := 50 157 | taskNum := 30000 158 | go processCallbackLoop(loop, taskNum, delay, beforeDiff, afterDiff) 159 | } 160 | 161 | func newTimeWheel() *timewheel.TimeWheel { 162 | tw, err := timewheel.NewTimeWheel(1*time.Second, 120) 163 | if err != nil { 164 | panic(err) 165 | } 166 | tw.Start() 167 | return tw 168 | } 169 | 170 | func processTimer(worker, delay, beforeDiff, afterDiff int) { 171 | for index := 0; index < worker; index++ { 172 | wg.Add(1) 173 | var ( 174 | htimer = tw.NewTimer(time.Duration(delay) * time.Second) 175 | incr = 0 176 | ) 177 | go func(idx int) { 178 | defer wg.Done() 179 | for incr < loopNum { 180 | now := time.Now() 181 | target := now.Add(time.Duration(delay) * time.Second) 182 | select { 183 | case <-htimer.C: 184 | htimer.Reset(time.Duration(delay) * time.Second) 185 | end := time.Now() 186 | if end.Before(target.Add(time.Duration(-beforeDiff) * time.Second)) { 187 | log.error("timer befer: %s, delay: %d", time.Since(now).String(), delay) 188 | } 189 | if end.After(target.Add(time.Duration(afterDiff) * time.Second)) { 190 | log.error("timer after: %s, delay: %d", time.Since(now).String(), delay) 191 | } 192 | 193 | incrCounter() 194 | // fmt.Println("id: ", idx, "cost: ", end.Sub(now)) 195 | } 196 | incr++ 197 | } 198 | }(index) 199 | } 200 | } 201 | 202 | func processCallbackLoop(loop, taskCount, delay, beforeDiff, afterDiff int) { 203 | wg.Add(1) 204 | for index := 0; index < loop; index++ { 205 | processCallback(taskCount, delay, beforeDiff, afterDiff) 206 | time.Sleep(time.Duration(delay) * time.Second) 207 | } 208 | wg.Done() 209 | } 210 | 211 | func processCallback(taskCount, delay, beforeDiff, afterDiff int) { 212 | for index := 0; index < taskCount; index++ { 213 | now := time.Now() 214 | cb := func() { 215 | target := now.Add(time.Duration(delay) * time.Second) 216 | end := time.Now() 217 | if end.Before(target.Add(time.Duration(-beforeDiff) * time.Second)) { 218 | log.error("cb befer: %s, delay: %d", time.Since(now).String(), delay) 219 | } 220 | if end.After(target.Add(time.Duration(afterDiff) * time.Second)) { 221 | log.error("cb after: %s, delay: %d", time.Since(now).String(), delay) 222 | } 223 | incrCounter() 224 | // log.info("cost: %s, delay: %d", end.Sub(now), delay) 225 | } 226 | tw.Add(time.Duration(delay)*time.Second, cb) 227 | } 228 | } 229 | 230 | func printCounter() { 231 | now := time.Now() 232 | for { 233 | n := atomic.LoadInt64(&counter) 234 | log.info("start_time: %s, since_time: %s, counter %d", now.String(), time.Since(now).String(), n) 235 | time.Sleep(10 * time.Second) 236 | } 237 | } 238 | 239 | type logger struct{} 240 | 241 | func (l *logger) info(format string, args ...interface{}) { 242 | v := fmt.Sprintf(format, args...) 243 | fmt.Println(v) 244 | } 245 | 246 | func (l *logger) error(format string, args ...interface{}) { 247 | v := fmt.Sprintf(format, args...) 248 | color := fmt.Sprintf("%c[%d;%d;%dm %s %c[0m", 0x1B, 5, 40, 31, v, 0x1B) 249 | fmt.Println(color) 250 | } 251 | -------------------------------------------------------------------------------- /example/simple/simple.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/rfyiamcool/go-timewheel" 8 | ) 9 | 10 | func main() { 11 | tw, err := timewheel.NewTimeWheel(200*time.Millisecond, 10) 12 | if err != nil { 13 | panic(err) 14 | } 15 | tw.Start() 16 | defer tw.Stop() 17 | 18 | count := 500000 19 | queue := make(chan bool, count) 20 | 21 | // loop 3 22 | for index := 0; index < 3; index++ { 23 | start := time.Now() 24 | for index := 0; index < count; index++ { 25 | tw.Add(time.Duration(1*time.Second), func() { 26 | queue <- true 27 | }) 28 | } 29 | fmt.Println("add timer cost: ", time.Since(start)) 30 | 31 | start = time.Now() 32 | incr := 0 33 | for { 34 | if incr == count { 35 | fmt.Println("recv sig cost: ", time.Since(start)) 36 | break 37 | } 38 | 39 | <-queue 40 | incr++ 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rfyiamcool/go-timewheel 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/davecgh/go-spew v1.1.1 // indirect 7 | github.com/kr/pretty v0.1.0 // indirect 8 | github.com/stretchr/testify v1.4.0 9 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 5 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 6 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 7 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 8 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 12 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 13 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 15 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 16 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 17 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 18 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 19 | -------------------------------------------------------------------------------- /task_pool.go: -------------------------------------------------------------------------------- 1 | package timewheel 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | var incr = 0 8 | 9 | var ( 10 | defaultTaskPool = newTaskPool() 11 | ) 12 | 13 | type taskPool struct { 14 | bp *sync.Pool 15 | } 16 | 17 | func newTaskPool() *taskPool { 18 | return &taskPool{ 19 | bp: &sync.Pool{ 20 | New: func() interface{} { 21 | return &Task{} 22 | }, 23 | }, 24 | } 25 | } 26 | 27 | func (pool *taskPool) get() *Task { 28 | return pool.bp.Get().(*Task) 29 | } 30 | 31 | func (pool *taskPool) put(obj *Task) { 32 | obj.Reset() 33 | pool.bp.Put(obj) 34 | } 35 | -------------------------------------------------------------------------------- /timer.go: -------------------------------------------------------------------------------- 1 | package timewheel 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "sync" 7 | "sync/atomic" 8 | "time" 9 | ) 10 | 11 | const ( 12 | typeTimer taskType = iota 13 | typeTicker 14 | 15 | modeIsCircle = true 16 | modeNotCircle = false 17 | 18 | modeIsAsync = true 19 | modeNotAsync = false 20 | ) 21 | 22 | type taskType int64 23 | type taskID int64 24 | 25 | type Task struct { 26 | delay time.Duration 27 | id taskID 28 | round int 29 | callback func() 30 | 31 | async bool 32 | stop bool 33 | circle bool 34 | // circleNum int 35 | } 36 | 37 | // for sync.Pool 38 | func (t *Task) Reset() { 39 | t.delay = 0 40 | t.id = 0 41 | t.round = 0 42 | t.callback = nil 43 | 44 | t.async = false 45 | t.stop = false 46 | t.circle = false 47 | } 48 | 49 | type optionCall func(*TimeWheel) error 50 | 51 | func TickSafeMode() optionCall { 52 | return func(o *TimeWheel) error { 53 | o.tickQueue = make(chan time.Time, 10) 54 | return nil 55 | } 56 | } 57 | 58 | // todo: 59 | // func SetSyncPool(state bool) optionCall { 60 | // return func(o *TimeWheel) error { 61 | // o.syncPool = state 62 | // return nil 63 | // } 64 | // } 65 | 66 | type TimeWheel struct { 67 | randomID int64 68 | 69 | tick time.Duration 70 | ticker *time.Ticker 71 | tickQueue chan time.Time 72 | 73 | bucketsNum int 74 | buckets []map[taskID]*Task // key: added item, value: *Task 75 | bucketIndexes map[taskID]int // key: added item, value: bucket position 76 | 77 | currentIndex int 78 | 79 | onceStart sync.Once 80 | 81 | stopC chan struct{} 82 | 83 | exited bool 84 | 85 | sync.RWMutex 86 | } 87 | 88 | // NewTimeWheel create new time wheel 89 | func NewTimeWheel(tick time.Duration, bucketsNum int, options ...optionCall) (*TimeWheel, error) { 90 | if tick.Milliseconds() < 1 { 91 | return nil, errors.New("invalid params, must tick >= 1 ms") 92 | } 93 | if bucketsNum <= 0 { 94 | return nil, errors.New("invalid params, must bucketsNum > 0") 95 | } 96 | 97 | tw := &TimeWheel{ 98 | // tick 99 | tick: tick, 100 | tickQueue: make(chan time.Time, 10), 101 | 102 | // store 103 | bucketsNum: bucketsNum, 104 | bucketIndexes: make(map[taskID]int, 1024*100), 105 | buckets: make([]map[taskID]*Task, bucketsNum), 106 | currentIndex: 0, 107 | 108 | // signal 109 | stopC: make(chan struct{}), 110 | } 111 | 112 | for i := 0; i < bucketsNum; i++ { 113 | tw.buckets[i] = make(map[taskID]*Task, 16) 114 | } 115 | 116 | for _, op := range options { 117 | op(tw) 118 | } 119 | 120 | return tw, nil 121 | } 122 | 123 | // Start start the time wheel 124 | func (tw *TimeWheel) Start() { 125 | // onlye once start 126 | tw.onceStart.Do( 127 | func() { 128 | tw.ticker = time.NewTicker(tw.tick) 129 | go tw.schduler() 130 | go tw.tickGenerator() 131 | }, 132 | ) 133 | } 134 | 135 | func (tw *TimeWheel) tickGenerator() { 136 | if tw.tickQueue != nil { 137 | return 138 | } 139 | 140 | for !tw.exited { 141 | select { 142 | case <-tw.ticker.C: 143 | select { 144 | case tw.tickQueue <- time.Now(): 145 | default: 146 | panic("raise long time blocking") 147 | } 148 | } 149 | } 150 | } 151 | 152 | func (tw *TimeWheel) schduler() { 153 | queue := tw.ticker.C 154 | if tw.tickQueue == nil { 155 | queue = tw.tickQueue 156 | } 157 | 158 | for { 159 | select { 160 | case <-queue: 161 | tw.handleTick() 162 | 163 | case <-tw.stopC: 164 | tw.exited = true 165 | tw.ticker.Stop() 166 | return 167 | } 168 | } 169 | } 170 | 171 | // Stop stop the time wheel 172 | func (tw *TimeWheel) Stop() { 173 | tw.stopC <- struct{}{} 174 | } 175 | 176 | func (tw *TimeWheel) collectTask(task *Task) { 177 | index := tw.bucketIndexes[task.id] 178 | delete(tw.bucketIndexes, task.id) 179 | delete(tw.buckets[index], task.id) 180 | 181 | // todo: 182 | // if tw.syncPool { 183 | // defaultTaskPool.put(task) 184 | // } 185 | } 186 | 187 | func (tw *TimeWheel) handleTick() { 188 | tw.Lock() 189 | defer tw.Unlock() 190 | 191 | bucket := tw.buckets[tw.currentIndex] 192 | for k, task := range bucket { 193 | if task.stop { 194 | tw.collectTask(task) 195 | continue 196 | } 197 | 198 | if bucket[k].round > 0 { 199 | bucket[k].round-- 200 | continue 201 | } 202 | 203 | if task.async { 204 | go task.callback() 205 | } else { 206 | // optimize gopool 207 | task.callback() 208 | } 209 | 210 | // circle 211 | if task.circle == true { 212 | tw.collectTask(task) 213 | tw.putCircle(task, modeIsCircle) 214 | continue 215 | } 216 | 217 | // gc 218 | tw.collectTask(task) 219 | } 220 | 221 | if tw.currentIndex == tw.bucketsNum-1 { 222 | tw.currentIndex = 0 223 | return 224 | } 225 | 226 | tw.currentIndex++ 227 | } 228 | 229 | // Add add an task 230 | func (tw *TimeWheel) Add(delay time.Duration, callback func()) *Task { 231 | return tw.addAny(delay, callback, modeNotCircle, modeIsAsync) 232 | } 233 | 234 | // AddCron add interval task 235 | func (tw *TimeWheel) AddCron(delay time.Duration, callback func()) *Task { 236 | return tw.addAny(delay, callback, modeIsCircle, modeIsAsync) 237 | } 238 | 239 | func (tw *TimeWheel) addAny(delay time.Duration, callback func(), circle, async bool) *Task { 240 | if delay <= 0 { 241 | delay = tw.tick 242 | } 243 | 244 | id := tw.genUniqueID() 245 | task := new(Task) 246 | 247 | // todo: 248 | // var task *Task 249 | // if tw.syncPool { 250 | // task = defaultTaskPool.get() 251 | // } 252 | 253 | task.delay = delay 254 | task.id = id 255 | task.callback = callback 256 | task.circle = circle 257 | task.async = async // refer to src/runtime/time.go 258 | 259 | tw.put(task) 260 | return task 261 | } 262 | 263 | func (tw *TimeWheel) put(task *Task) { 264 | tw.Lock() 265 | defer tw.Unlock() 266 | 267 | tw.store(task, false) 268 | } 269 | 270 | func (tw *TimeWheel) putCircle(task *Task, circleMode bool) { 271 | tw.store(task, circleMode) 272 | } 273 | 274 | func (tw *TimeWheel) store(task *Task, circleMode bool) { 275 | round := tw.calculateRound(task.delay) 276 | index := tw.calculateIndex(task.delay) 277 | 278 | if round > 0 && circleMode { 279 | task.round = round - 1 280 | } else { 281 | task.round = round 282 | } 283 | 284 | tw.bucketIndexes[task.id] = index 285 | tw.buckets[index][task.id] = task 286 | } 287 | 288 | func (tw *TimeWheel) calculateRound(delay time.Duration) (round int) { 289 | delaySeconds := delay.Seconds() 290 | tickSeconds := tw.tick.Seconds() 291 | round = int(delaySeconds / tickSeconds / float64(tw.bucketsNum)) 292 | return 293 | } 294 | 295 | func (tw *TimeWheel) calculateIndex(delay time.Duration) (index int) { 296 | delaySeconds := delay.Seconds() 297 | tickSeconds := tw.tick.Seconds() 298 | index = (int(float64(tw.currentIndex) + delaySeconds/tickSeconds)) % tw.bucketsNum 299 | return 300 | } 301 | 302 | func (tw *TimeWheel) Remove(task *Task) error { 303 | // tw.removeC <- task 304 | tw.remove(task) 305 | return nil 306 | } 307 | 308 | func (tw *TimeWheel) remove(task *Task) { 309 | tw.Lock() 310 | defer tw.Unlock() 311 | 312 | tw.collectTask(task) 313 | } 314 | 315 | func (tw *TimeWheel) NewTimer(delay time.Duration) *Timer { 316 | queue := make(chan bool, 1) // buf = 1, refer to src/time/sleep.go 317 | task := tw.addAny(delay, 318 | func() { 319 | notifyChannel(queue) 320 | }, 321 | modeNotCircle, 322 | modeNotAsync, 323 | ) 324 | 325 | // init timer 326 | ctx, cancel := context.WithCancel(context.Background()) 327 | timer := &Timer{ 328 | tw: tw, 329 | C: queue, // faster 330 | task: task, 331 | Ctx: ctx, 332 | cancel: cancel, 333 | } 334 | 335 | return timer 336 | } 337 | 338 | func (tw *TimeWheel) AfterFunc(delay time.Duration, callback func()) *Timer { 339 | queue := make(chan bool, 1) 340 | task := tw.addAny(delay, 341 | func() { 342 | callback() 343 | notifyChannel(queue) 344 | }, 345 | modeNotCircle, modeIsAsync, 346 | ) 347 | 348 | // init timer 349 | ctx, cancel := context.WithCancel(context.Background()) 350 | timer := &Timer{ 351 | tw: tw, 352 | C: queue, // faster 353 | task: task, 354 | Ctx: ctx, 355 | cancel: cancel, 356 | fn: callback, 357 | } 358 | 359 | return timer 360 | } 361 | 362 | func (tw *TimeWheel) NewTicker(delay time.Duration) *Ticker { 363 | queue := make(chan bool, 1) 364 | task := tw.addAny(delay, 365 | func() { 366 | notifyChannel(queue) 367 | }, 368 | modeIsCircle, 369 | modeNotAsync, 370 | ) 371 | 372 | // init ticker 373 | ctx, cancel := context.WithCancel(context.Background()) 374 | ticker := &Ticker{ 375 | task: task, 376 | tw: tw, 377 | C: queue, 378 | Ctx: ctx, 379 | cancel: cancel, 380 | } 381 | 382 | return ticker 383 | } 384 | 385 | func (tw *TimeWheel) After(delay time.Duration) <-chan time.Time { 386 | queue := make(chan time.Time, 1) 387 | tw.addAny(delay, 388 | func() { 389 | queue <- time.Now() 390 | }, 391 | modeNotCircle, modeNotAsync, 392 | ) 393 | return queue 394 | } 395 | 396 | func (tw *TimeWheel) Sleep(delay time.Duration) { 397 | queue := make(chan bool, 1) 398 | tw.addAny(delay, 399 | func() { 400 | queue <- true 401 | }, 402 | modeNotCircle, modeNotAsync, 403 | ) 404 | <-queue 405 | } 406 | 407 | // similar to golang std timer 408 | type Timer struct { 409 | task *Task 410 | tw *TimeWheel 411 | fn func() // external custom func 412 | stopFn func() // call function when timer stop 413 | 414 | C chan bool 415 | 416 | cancel context.CancelFunc 417 | Ctx context.Context 418 | } 419 | 420 | func (t *Timer) Reset(delay time.Duration) { 421 | // first stop old task 422 | t.task.stop = true 423 | 424 | // make new task 425 | var task *Task 426 | if t.fn != nil { // use AfterFunc 427 | task = t.tw.addAny(delay, 428 | func() { 429 | t.fn() 430 | notifyChannel(t.C) 431 | }, 432 | modeNotCircle, modeIsAsync, // must async mode 433 | ) 434 | } else { 435 | task = t.tw.addAny(delay, 436 | func() { 437 | notifyChannel(t.C) 438 | }, 439 | modeNotCircle, modeNotAsync) 440 | } 441 | 442 | t.task = task 443 | } 444 | 445 | func (t *Timer) Stop() { 446 | if t.stopFn != nil { 447 | t.stopFn() 448 | } 449 | 450 | t.task.stop = true 451 | t.cancel() 452 | t.tw.Remove(t.task) 453 | } 454 | 455 | func (t *Timer) AddStopFunc(callback func()) { 456 | t.stopFn = callback 457 | } 458 | 459 | type Ticker struct { 460 | tw *TimeWheel 461 | task *Task 462 | cancel context.CancelFunc 463 | 464 | C chan bool 465 | Ctx context.Context 466 | } 467 | 468 | func (t *Ticker) Stop() { 469 | t.task.stop = true 470 | t.cancel() 471 | t.tw.Remove(t.task) 472 | } 473 | 474 | func notifyChannel(q chan bool) { 475 | select { 476 | case q <- true: 477 | default: 478 | } 479 | } 480 | 481 | func (tw *TimeWheel) genUniqueID() taskID { 482 | id := atomic.AddInt64(&tw.randomID, 1) 483 | return taskID(id) 484 | } 485 | -------------------------------------------------------------------------------- /timer_test.go: -------------------------------------------------------------------------------- 1 | package timewheel 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "sync/atomic" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func callback() { 14 | fmt.Println("callback !!!") 15 | } 16 | 17 | func checkTimeCost(t *testing.T, start, end time.Time, before int, after int) bool { 18 | due := end.Sub(start) 19 | if due > time.Duration(after)*time.Millisecond { 20 | t.Error("delay run") 21 | return false 22 | } 23 | 24 | if due < time.Duration(before)*time.Millisecond { 25 | t.Error("run ahead") 26 | return false 27 | } 28 | 29 | return true 30 | } 31 | 32 | func TestCalcPos(t *testing.T) { 33 | tw, _ := NewTimeWheel(100*time.Millisecond, 5) 34 | round := tw.calculateRound(1 * time.Second) 35 | if round != 2 { 36 | t.Error("round err") 37 | } 38 | 39 | idx := tw.calculateIndex(1 * time.Second) 40 | if idx != 0 { 41 | t.Error("idx err") 42 | } 43 | } 44 | 45 | func TestAddFunc(t *testing.T) { 46 | tw, _ := NewTimeWheel(100*time.Millisecond, 5, TickSafeMode()) 47 | tw.Start() 48 | defer tw.Stop() 49 | 50 | for index := 1; index < 6; index++ { 51 | queue := make(chan bool, 0) 52 | start := time.Now() 53 | tw.Add(time.Duration(index)*time.Second, func() { 54 | queue <- true 55 | }) 56 | 57 | <-queue 58 | 59 | before := index*1000 - 200 60 | after := index*1000 + 200 61 | checkTimeCost(t, start, time.Now(), before, after) 62 | fmt.Println("time since: ", time.Since(start).String()) 63 | } 64 | } 65 | 66 | func TestAddStopCron(t *testing.T) { 67 | tw, _ := NewTimeWheel(100*time.Millisecond, 60) 68 | tw.Start() 69 | defer tw.Stop() 70 | 71 | queue := make(chan time.Time, 2) 72 | task := tw.AddCron(time.Second*1, func() { 73 | queue <- time.Now() 74 | }) 75 | 76 | time.AfterFunc(5*time.Second, func() { 77 | tw.Remove(task) 78 | }) 79 | 80 | exitTimer := time.NewTimer(10 * time.Second) 81 | lastTs := time.Now() 82 | c := 0 83 | for { 84 | select { 85 | case <-exitTimer.C: 86 | if c > 6 { 87 | t.Error("cron stop failed") 88 | } 89 | return 90 | 91 | case now := <-queue: 92 | c++ 93 | checkTimeCost(t, lastTs, now, 900, 1200) 94 | fmt.Println("time since: ", now.Sub(lastTs)) 95 | lastTs = now 96 | } 97 | } 98 | } 99 | 100 | func TestStopTimer(t *testing.T) { 101 | tw, _ := NewTimeWheel(100*time.Millisecond, 5) 102 | tw.Start() 103 | defer tw.Stop() 104 | 105 | timer := tw.NewTimer(time.Millisecond * 500) 106 | 107 | exitTimer := time.NewTimer(2 * time.Second) 108 | timer.Stop() 109 | 110 | select { 111 | case <-exitTimer.C: 112 | case <-timer.C: 113 | t.Error("must not run") 114 | } 115 | } 116 | 117 | func TestStopTicker1(t *testing.T) { 118 | tw, _ := NewTimeWheel(100*time.Millisecond, 100) 119 | tw.Start() 120 | defer tw.Stop() 121 | 122 | timer := tw.NewTicker(time.Millisecond * 500) 123 | timer.Stop() 124 | 125 | time.Sleep(1 * time.Second) 126 | 127 | select { 128 | case <-timer.C: 129 | t.Error("exception") 130 | default: 131 | } 132 | 133 | select { 134 | case <-timer.Ctx.Done(): 135 | return 136 | case <-time.After(1 * time.Second): 137 | t.Error("exception") 138 | } 139 | } 140 | 141 | func TestStopTicker2(t *testing.T) { 142 | tw, _ := NewTimeWheel(100*time.Millisecond, 100) 143 | tw.Start() 144 | defer tw.Stop() 145 | 146 | var tickers [1000]*Ticker 147 | for idx := range tickers { 148 | ticker := tw.NewTicker(time.Millisecond * 500) 149 | tickers[idx] = ticker 150 | } 151 | 152 | for _, ticker := range tickers { 153 | ticker.Stop() 154 | } 155 | 156 | time.Sleep(1 * time.Second) 157 | assert.Equal(t, 0, len(tw.bucketIndexes)) 158 | } 159 | 160 | func TestTickerBetween(t *testing.T) { 161 | tw, _ := NewTimeWheel(100*time.Millisecond, 1000) 162 | tw.Start() 163 | defer tw.Stop() 164 | 165 | ticker := tw.NewTicker(100 * time.Millisecond) 166 | 167 | time.AfterFunc(5*time.Second, func() { 168 | ticker.Stop() 169 | }) 170 | 171 | exitTimer := time.After(3 * time.Second) 172 | last := time.Now() 173 | for c := 0; c < 6; { 174 | select { 175 | case <-ticker.C: 176 | t.Log("since", time.Since(last)) 177 | last = time.Now() 178 | c++ 179 | 180 | case <-exitTimer: 181 | return 182 | } 183 | } 184 | } 185 | 186 | func TestTickerSecond(t *testing.T) { 187 | tw, err := NewTimeWheel(1*time.Millisecond, 10000) 188 | assert.Nil(t, err) 189 | 190 | tw.Start() 191 | defer tw.Stop() 192 | 193 | var ( 194 | timeout = time.After(110 * time.Millisecond) 195 | ticker = tw.NewTicker(1 * time.Millisecond) 196 | incr int 197 | ) 198 | 199 | for run := true; run; { 200 | select { 201 | case <-timeout: 202 | run = false 203 | 204 | case <-ticker.C: 205 | incr++ 206 | } 207 | } 208 | 209 | assert.Greater(t, incr, 100) 210 | } 211 | 212 | func TestBatchTicker(t *testing.T) { 213 | tw, err := NewTimeWheel(100*time.Millisecond, 60) 214 | assert.Nil(t, err) 215 | 216 | tw.Start() 217 | defer tw.Stop() 218 | 219 | wg := sync.WaitGroup{} 220 | for index := 0; index < 100; index++ { 221 | wg.Add(1) 222 | go func() { 223 | defer wg.Done() 224 | ticker := tw.NewTicker(1 * time.Second) 225 | go func() { 226 | time.Sleep(2 * time.Second) 227 | ticker.Stop() 228 | }() 229 | 230 | for { 231 | select { 232 | case <-ticker.C: 233 | case <-ticker.Ctx.Done(): 234 | return 235 | } 236 | } 237 | }() 238 | } 239 | wg.Wait() 240 | } 241 | 242 | func TestAfter(t *testing.T) { 243 | tw, _ := NewTimeWheel(100*time.Millisecond, 10) 244 | tw.Start() 245 | defer tw.Stop() 246 | 247 | for index := 1; index < 6; index++ { 248 | ts := time.Now() 249 | <-tw.After(time.Duration(index) * time.Second) 250 | before := index*1000 - 200 251 | after := index*1000 + 200 252 | checkTimeCost(t, ts, time.Now(), before, after) 253 | } 254 | } 255 | 256 | func TestAfterFunc(t *testing.T) { 257 | tw, _ := NewTimeWheel(100*time.Millisecond, 10) 258 | tw.Start() 259 | defer tw.Stop() 260 | 261 | queue := make(chan bool, 1) 262 | timer := tw.AfterFunc(1*time.Second, func() { 263 | queue <- true 264 | }) 265 | <-queue 266 | 267 | for index := 1; index < 6; index++ { 268 | timer.Reset(time.Duration(index) * time.Second) 269 | ts := time.Now() 270 | <-queue 271 | before := index*1000 - 200 272 | after := index*1000 + 200 273 | checkTimeCost(t, ts, time.Now(), before, after) 274 | fmt.Println(time.Since(ts).String()) 275 | } 276 | } 277 | 278 | func TestAfterFuncResetStop(t *testing.T) { 279 | tw, _ := NewTimeWheel(100*time.Millisecond, 10) 280 | tw.Start() 281 | defer tw.Stop() 282 | 283 | var incr = 0 284 | 285 | // stop 286 | timer := tw.AfterFunc(100*time.Millisecond, func() { 287 | incr++ 288 | }) 289 | timer.Stop() 290 | 291 | time.Sleep(500 * time.Millisecond) 292 | assert.Equal(t, 0, incr) 293 | 294 | // reset 295 | incr = 0 296 | timer = tw.AfterFunc(800*time.Millisecond, func() { 297 | incr++ 298 | }) 299 | timer.Reset(100 * time.Millisecond) 300 | time.Sleep(300 * time.Millisecond) 301 | assert.Equal(t, incr, 1) 302 | 303 | // reset stop 304 | incr = 0 305 | timer = tw.AfterFunc(100*time.Millisecond, func() { 306 | incr++ 307 | }) 308 | timer.Reset(100 * time.Millisecond) 309 | timer.Reset(100 * time.Millisecond) 310 | timer.Reset(1000 * time.Millisecond) 311 | time.Sleep(500 * time.Millisecond) 312 | assert.Equal(t, 0, incr) 313 | time.Sleep(700 * time.Millisecond) 314 | assert.Equal(t, 1, incr) 315 | } 316 | 317 | func TestTimerReset(t *testing.T) { 318 | tw, _ := NewTimeWheel(100*time.Millisecond, 5) 319 | tw.Start() 320 | defer tw.Stop() 321 | 322 | timer := tw.NewTimer(100 * time.Millisecond) 323 | now := time.Now() 324 | <-timer.C 325 | fmt.Println(time.Since(now).String()) 326 | checkTimeCost(t, now, time.Now(), 80, 220) 327 | 328 | for index := 1; index < 6; index++ { 329 | now := time.Now() 330 | timer.Reset(time.Duration(index) * time.Second) 331 | <-timer.C 332 | 333 | before := index*1000 - 200 334 | after := index*1000 + 200 335 | 336 | checkTimeCost(t, now, time.Now(), before, after) 337 | } 338 | } 339 | 340 | func TestRemove(t *testing.T) { 341 | tw, _ := NewTimeWheel(100*time.Millisecond, 5) 342 | tw.Start() 343 | defer tw.Stop() 344 | 345 | queue := make(chan bool, 0) 346 | task := tw.Add(time.Millisecond*500, func() { 347 | queue <- true 348 | }) 349 | 350 | // remove action after add action 351 | time.AfterFunc(time.Millisecond*10, func() { 352 | tw.Remove(task) 353 | }) 354 | 355 | exitTimer := time.NewTimer(1 * time.Second) 356 | select { 357 | case <-exitTimer.C: 358 | case <-queue: 359 | t.Error("must not run") 360 | } 361 | } 362 | 363 | func TestHwTimer(t *testing.T) { 364 | tw, _ := NewTimeWheel(100*time.Millisecond, 60) 365 | tw.Start() 366 | defer tw.Stop() 367 | 368 | worker := 10 369 | 370 | wg := sync.WaitGroup{} 371 | for index := 0; index < worker; index++ { 372 | wg.Add(1) 373 | var ( 374 | htimer = tw.NewTimer(1 * time.Second) 375 | maxnum = 5 376 | incr = 0 377 | ) 378 | go func(idx int) { 379 | defer wg.Done() 380 | for incr < maxnum { 381 | now := time.Now() 382 | select { 383 | case <-htimer.C: 384 | htimer.Reset(1 * time.Second) 385 | end := time.Now() 386 | checkTimeCost(t, now, end, 900, 1200) 387 | } 388 | incr++ 389 | } 390 | }(index) 391 | } 392 | wg.Wait() 393 | } 394 | 395 | func BenchmarkAdd(b *testing.B) { 396 | tw, _ := NewTimeWheel(100*time.Millisecond, 60) 397 | tw.Start() 398 | defer tw.Stop() 399 | 400 | for i := 0; i < b.N; i++ { 401 | tw.Add(time.Second, func() {}) 402 | } 403 | } 404 | 405 | func TestRunStopFunc(t *testing.T) { 406 | var ( 407 | t1 = NewTimer(time.Second * 1) 408 | called bool 409 | ) 410 | 411 | t1.AddStopFunc(func() { 412 | called = true 413 | }) 414 | 415 | select { 416 | case <-t1.C: 417 | t1.Stop() 418 | } 419 | 420 | assert.Equal(t, called, true) 421 | } 422 | 423 | func TestResetStopWithSec(t *testing.T) { 424 | tw, err := NewTimeWheel(1*time.Second, 1000) 425 | assert.Nil(t, err) 426 | 427 | tw.Start() 428 | defer tw.Stop() 429 | 430 | var ( 431 | timers = make([]*Timer, 1000) 432 | incr int64 433 | ) 434 | 435 | for i := 0; i < len(timers); i++ { 436 | timers[i] = tw.AfterFunc(time.Duration(i)*time.Millisecond, func() { 437 | atomic.AddInt64(&incr, 1) 438 | }) 439 | } 440 | 441 | for i := 0; i < len(timers); i++ { 442 | timers[i].Stop() 443 | } 444 | 445 | time.Sleep(3 * time.Second) 446 | assert.Equal(t, 0, len(tw.bucketIndexes)) 447 | assert.EqualValues(t, 0, incr) 448 | 449 | for i := 0; i < len(timers); i++ { 450 | i := i 451 | go func() { 452 | tw.AfterFunc(time.Duration(i)*time.Millisecond, func() { 453 | atomic.AddInt64(&incr, 1) 454 | }) 455 | }() 456 | } 457 | 458 | time.Sleep(3 * time.Second) 459 | assert.EqualValues(t, 1000, incr) 460 | } 461 | 462 | func TestResetStop2WithMill(t *testing.T) { 463 | tw, err := NewTimeWheel(100*time.Millisecond, 1000) 464 | assert.Nil(t, err) 465 | 466 | tw.Start() 467 | defer tw.Stop() 468 | 469 | var ( 470 | count = 1000 471 | incr int64 472 | ) 473 | 474 | for i := 0; i < count; i++ { 475 | i := i 476 | go func() { 477 | tw.AfterFunc(time.Duration(i)*time.Millisecond, func() { 478 | atomic.AddInt64(&incr, 1) 479 | }) 480 | }() 481 | } 482 | 483 | time.Sleep(2 * time.Second) 484 | assert.EqualValues(t, count, incr) 485 | } 486 | 487 | func TestAddRemove(t *testing.T) { 488 | tw, err := NewTimeWheel(100*time.Millisecond, 10000, TickSafeMode()) 489 | assert.Equal(t, nil, err) 490 | 491 | tw.Start() 492 | defer tw.Stop() 493 | 494 | var incr int64 495 | for i := 0; i < 1000; i++ { 496 | task := tw.Add(1*time.Second, func() { 497 | atomic.AddInt64(&incr, 1) 498 | }) 499 | 500 | tw.Remove(task) 501 | } 502 | 503 | time.Sleep(2 * time.Second) 504 | assert.EqualValues(t, 0, incr) 505 | 506 | for i := 0; i < 1000; i++ { 507 | tw.Add(1*time.Second, func() { 508 | atomic.AddInt64(&incr, 1) 509 | }) 510 | } 511 | 512 | time.Sleep(2 * time.Second) 513 | assert.EqualValues(t, 1000, incr) 514 | } 515 | -------------------------------------------------------------------------------- /timewheel.go: -------------------------------------------------------------------------------- 1 | package timewheel 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | var ( 8 | DefaultTimeWheel, _ = NewTimeWheel(time.Second, 120) 9 | ) 10 | 11 | func init() { 12 | DefaultTimeWheel.Start() 13 | } 14 | 15 | func ResetDefaultTimeWheel(tw *TimeWheel) { 16 | tw.Start() 17 | DefaultTimeWheel = tw 18 | } 19 | 20 | func Add(delay time.Duration, callback func()) *Task { 21 | return DefaultTimeWheel.Add(delay, callback) 22 | } 23 | 24 | func AddCron(delay time.Duration, callback func()) *Task { 25 | return DefaultTimeWheel.AddCron(delay, callback) 26 | } 27 | 28 | func Remove(task *Task) error { 29 | return DefaultTimeWheel.Remove(task) 30 | } 31 | 32 | func NewTimer(delay time.Duration) *Timer { 33 | return DefaultTimeWheel.NewTimer(delay) 34 | } 35 | 36 | func NewTicker(delay time.Duration) *Ticker { 37 | return DefaultTimeWheel.NewTicker(delay) 38 | } 39 | 40 | func AfterFunc(delay time.Duration, callback func()) *Timer { 41 | return DefaultTimeWheel.AfterFunc(delay, callback) 42 | } 43 | 44 | func After(delay time.Duration) <-chan time.Time { 45 | return DefaultTimeWheel.After(delay) 46 | } 47 | 48 | func Sleep(delay time.Duration) { 49 | DefaultTimeWheel.Sleep(delay) 50 | } 51 | -------------------------------------------------------------------------------- /timewheel_pool.go: -------------------------------------------------------------------------------- 1 | package timewheel 2 | 3 | import ( 4 | "math/rand" 5 | "sync/atomic" 6 | "time" 7 | ) 8 | 9 | type TimeWheelPool struct { 10 | pool []*TimeWheel 11 | size int64 12 | incr int64 // not need for high accuracy 13 | } 14 | 15 | func NewTimeWheelPool(size int, tick time.Duration, bucketsNum int, options ...optionCall) (*TimeWheelPool, error) { 16 | twp := &TimeWheelPool{ 17 | pool: make([]*TimeWheel, size), 18 | size: int64(size), 19 | } 20 | 21 | for index := 0; index < bucketsNum; index++ { 22 | tw, err := NewTimeWheel(tick, bucketsNum, options...) 23 | if err != nil { 24 | return twp, err 25 | } 26 | 27 | twp.pool[index] = tw 28 | } 29 | 30 | return twp, nil 31 | } 32 | 33 | func (tp *TimeWheelPool) Get() *TimeWheel { 34 | incr := atomic.AddInt64(&tp.incr, 1) 35 | idx := incr % tp.size 36 | return tp.pool[idx] 37 | } 38 | 39 | func (tp *TimeWheelPool) GetRandom() *TimeWheel { 40 | rand.Seed(time.Now().UnixNano()) 41 | idx := rand.Int63n(tp.size) 42 | return tp.pool[idx] 43 | } 44 | 45 | func (tp *TimeWheelPool) Start() { 46 | for _, tw := range tp.pool { 47 | tw.Start() 48 | } 49 | } 50 | 51 | func (tp *TimeWheelPool) Stop() { 52 | for _, tw := range tp.pool { 53 | tw.Stop() 54 | } 55 | } 56 | --------------------------------------------------------------------------------