10

I would like to change my ticker interval dynamically.

I've written down an example to show you how I did. My use case is something else than an "accelerometer" but I hope that it gives you an idea.

http://play.golang.org/p/6ANFnoE6pA

package main

import (
    "time"
    "log"
    "fmt"
)

func main() {
    interval := float64(1000)

    ticker := time.NewTicker(time.Duration(interval) * time.Millisecond)
    go func(){
        counter := 1.0
        for range ticker.C {
            log.Println("ticker accelerating to " + fmt.Sprint(interval/counter) + " ms")
            ticker = time.NewTicker(time.Duration(interval/counter) * time.Millisecond)
            counter++
        }
        log.Println("stopped")
    }()
    time.Sleep(5 * time.Second)
    log.Println("stopping ticker")
    ticker.Stop()
}

What is wrong is that the ticker will always "tick" every seconds and it doesn't accelerate... Any idea?

damoiser
  • 6,058
  • 3
  • 40
  • 66
  • The code has data race – Bleeding Fingers Apr 18 '16 at 09:21
  • Because the for loop is still using the channel from the old ticker object and not using the channels from the new ticker objects. – Nipun Talukdar Apr 18 '16 at 09:36
  • Thanks for your feedbacks. @BleedingFingers I see the data race (on the ticker var), but in this case shouldn't get a panic? Otherwise the pointer should be replaced with the new one. @ NipunTalukdar I have thought the same too, if this is right - then means that the ticker pointer is "cached" in the range-loop and overriding is not possible. I will try this with another example. – damoiser Apr 18 '16 at 09:42
  • Yep, @NipunTalukdar, I guess that the ```range``` method caches the variable to be looped, then I think that overidding of the ticker like I suggested (using ```range```) is not possible - http://play.golang.org/p/yZvrgURz4o – damoiser Apr 18 '16 at 10:18

4 Answers4

8

Following the answer to @fzerorubigd but a little more complete.

As said before, we can't use the range for this case, because the range loop caches the variable to be lopped and then it can't be overwritten (example here: http://play.golang.org/p/yZvrgURz4o )

Then, we should use a for-select combination loop. Hereafter the working solution:

http://play.golang.org/p/3uJrAIhnTQ

package main

import (
    "time"
    "log"
    "fmt"
)

func main() {
    start_interval := float64(1000)
    quit := make(chan bool)

    go func(){
        ticker := time.NewTicker(time.Duration(start_interval) * time.Millisecond)
        counter := 1.0

        for {
            select {
            case <-ticker.C:
                log.Println("ticker accelerating to " + fmt.Sprint(start_interval/counter) + " ms")
                ticker.Stop()
                ticker = time.NewTicker(time.Duration(start_interval/counter) * time.Millisecond)
                counter++
            case <-quit:
                ticker.Stop()
                log.Println("..ticker stopped!")
                return
            }
        }
    }()

    time.Sleep(5 * time.Second)

    log.Println("stopping ticker...")
    quit<-true

    time.Sleep(500 * time.Millisecond) // just to see quit messages
}
damoiser
  • 6,058
  • 3
  • 40
  • 66
  • 1
    Will creating new `time.NewTicker` increase overhead of the garbage collection? – chinuy Feb 20 '18 at 19:51
  • 1
    good question @chinuy - I would say that GC is needed there even if I am doing a replacement of the var ```ticker```. If you look at the code of ```time.NewTicker``` [HERE](https://golang.org/src/time/tick.go?s=706:740#L11) you can see that it creates some local structs that need to be collected at some point by the garbage collector. In conclusion: yes, it increase the overhead of GC – damoiser Feb 20 '18 at 21:16
5

that why in go1.15 ticker.Reset is Introduced, you don't need to create a new ticker update the existing ticker's duration with ticker.Reset("new duration"), and now you will not have any cache issues

Go playground

package main

import (
    "fmt"
    "log"
    "time"
)

func main() {
    interval := float64(1000)

    ticker := time.NewTicker(time.Duration(interval) * time.Millisecond)
    go func(){
        counter := 1.0
        for range ticker.C {
            log.Println("ticker accelerating to " + fmt.Sprint(interval/counter) + " ms")
            ticker.Reset(time.Duration(interval/counter) * time.Millisecond)
            counter++
        }
        log.Println("stopped")
    }()
    time.Sleep(5 * time.Second)
    log.Println("stopping ticker")
    ticker.Stop()
}

The reason your example have a cache issue is that when you reassign the ticker variable with a *time.ticker struct you just unlink the original *time.ticker struct from the ticker variable but the loop is still tide to the original ticker channel you need to reassin a new loop to the new time.ticker.c

Isaac Weingarten
  • 977
  • 10
  • 15
  • Although reset does work, the [documentation](https://golang.org/pkg/time/#Timer.Reset) states `Reset should be invoked only on stopped or expired timers with drained channels.` So I wouldn't suggest resetting an active ticker. – Chen A. Oct 24 '20 at 19:55
  • 1
    you confusing it with `time.timer{}.Reset()` on `time.tiker{}.Reset()` dosn't have this worning, here is the [documentation](https://golang.org/pkg/time/#Ticker.Reset) for it – Isaac Weingarten Oct 25 '20 at 23:34
  • It's worth noting `ticker.Reset` method is available from go1.15. – Chen A. Oct 30 '20 at 07:47
  • Please explain what’s your problem with go1.15? The compiler will work with older code – Isaac Weingarten Nov 01 '20 at 22:42
  • @Issac it's not a problem, but worth noting. I used earlier version (1.14) and it didn' work. – Chen A. Nov 03 '20 at 14:38
1

As Nipun Talukdar mentioned, the "for" capture the channel and use the same reference for iterate. it fixed if you use it like this :

playground

package main

import (
    "fmt"
    "log"
    "time"
)

func main() {
    interval := float64(1000)

    ticker := time.NewTicker(time.Duration(interval) * time.Millisecond)
    go func() {
        counter := 1.0
        for {
            select {
            case <-ticker.C:
                log.Println("ticker accelerating to " + fmt.Sprint(interval/counter) + " ms")
                ticker = time.NewTicker(time.Duration(interval/counter) * time.Millisecond)
                counter++
            }
        }
        log.Println("stopped")
    }()
    time.Sleep(5 * time.Second)
    log.Println("stopping ticker")
    ticker.Stop()
}
fzerorubigd
  • 1,664
  • 2
  • 16
  • 23
  • Thanks for your answer. Yep, I thought too that using ```for```-loop instead ```range```-loop can solve the issue. Just pay attention that when using ```for```-loop you should manage the incoming channel with a ```select```-catcher. To avoid confusing other developers - I will accept your answer if you edit the ```for```-code using the correctly ```select```-cases. – damoiser Apr 18 '16 at 10:14
  • The select is not doing anything special when the case is single. but I get the idea and the code is updated. – fzerorubigd Apr 18 '16 at 10:42
  • my feedback was related to the "exit" function - to release all the variables used in the goroutines the ```for```-loop should return. This happens even in single-case. Btw it was not asked, want just to be more precise ;-) – damoiser Apr 18 '16 at 11:11
  • 3
    It is worth noticing that when you call ticker.Stop() you don't actually close its channel, so the `log.Println("stopped")` line will never be reached. You should keep a secondary channel to signal when you're done and then `select` between the ticker and the secondary channel. You should also be stopping every old ticker before creating a new one. – hbejgel Apr 18 '16 at 19:39
  • @hbejgel the ticker.C is a receive-only channel. you can not close a receive-only channel. the owner of channel who has the whole ref to it can close it. see https://golang.org/pkg/time/#Ticker – fzerorubigd Apr 19 '16 at 04:15
  • I am aware of this, and because of that you will never escape the `for` loop inside the goroutine. Therefore the goroutine will never print the expected line `stopped` – hbejgel Apr 19 '16 at 19:09
0

What about this code:

https://play.golang.org/p/wyOTVxUW5Xj

package main

import (
    "fmt"
    "log"
    "time"
)

func main() {
    startInterval := float64(1000)
    quit := make(chan bool)

    go func() {
        counter := 1.0
        for {
            select {
            case <-time.After(time.Duration(startInterval/counter) * time.Millisecond):
                log.Println("ticker accelerating to " + fmt.Sprint(startInterval/counter) + " ms")
                counter++
            case <-quit:
                log.Println("..ticker stopped!")
                return
            }
        }
    }()

    time.Sleep(5 * time.Second)
    log.Println("stopping ticker...")
    quit <- true
    time.Sleep(500 * time.Millisecond) // just to see quit messages
}
sgon00
  • 4,879
  • 1
  • 43
  • 59