2

I have a list of functions and their respective intervals. I want to run each function at its interval concurrently.

In JavaScript, I wrote something like:

maps.forEach(({fn, interval}) => {
    setInterval(fn, interval)
})

How do I implement this functionality in Golang?

icza
  • 389,944
  • 63
  • 907
  • 827
Chidi Williams
  • 399
  • 6
  • 16

1 Answers1

14

Use a time.Ticker to receive "events" periodically, which you may use to time the execution of a function. You may obtain a time.Ticker by calling time.NewTicker(). The returned ticker has a channel on which values are sent periodically.

Use a goroutine to continuously receive the events and call the function, e.g. with a for range loop.

Let's see 2 functions:

func oneSec() {
    log.Println("oneSec")
}

func twoSec() {
    log.Println("twoSec")
}

Here's a simple scheduler that periodically calls a given function:

func schedule(f func(), interval time.Duration) *time.Ticker {
    ticker := time.NewTicker(interval)
    go func() {
        for range ticker.C {
            f()
        }
    }()
    return ticker
}

Example using it:

func main() {
    t1 := schedule(oneSec, time.Second)
    t2 := schedule(twoSec, 2*time.Second)
    time.Sleep(5 * time.Second)
    t1.Stop()
    t2.Stop()
}

Example output (try it on the Go Playground):

2009/11/10 23:00:01 oneSec
2009/11/10 23:00:02 twoSec
2009/11/10 23:00:02 oneSec
2009/11/10 23:00:03 oneSec
2009/11/10 23:00:04 twoSec
2009/11/10 23:00:04 oneSec

Note that Ticker.Stop() does not close the ticker's channel, so a for range will not terminate; Stop() only stops sending values on the ticker's channel.

If you want to terminate the goroutines used to schedule the function calls, you may do that with an additional channel. And then those goroutines may use a select statement to "monitor" the ticker's channel and this done channel, and return if receiving from done succeeds.

For example:

func schedule(f func(), interval time.Duration, done <-chan bool) *time.Ticker {
    ticker := time.NewTicker(interval)
    go func() {
        for {
            select {
            case <-ticker.C:
                f()
            case <-done:
                return
            }
        }
    }()
    return ticker
}

And using it:

func main() {
    done := make(chan bool)
    t1 := schedule(oneSec, time.Second, done)
    t2 := schedule(twoSec, 2*time.Second, done)
    time.Sleep(5 * time.Second)
    close(done)
    t1.Stop()
    t2.Stop()
}

Try this one on the Go Playground.

Note that even though stopping the tickers is not necessary in this simple example (because when the main goroutine ends, so does the program with it), in real-life examples if the app continues to run, leaving the tickers unstopped wastes resources (they will continue to use a background goroutine, and will continue to try to send values on their channels).

Last words:

If you have a slice of function-interval pairs, simply use a loop to pass each pair to this schedule() function. Something like this:

type pair struct {
    f        func()
    interval time.Duration
}

pairs := []pair{
    {oneSec, time.Second},
    {twoSec, 2 * time.Second},
}

done := make(chan bool)
ts := make([]*time.Ticker, len(pairs))
for i, p := range pairs {
    ts[i] = schedule(p.f, p.interval, done)
}

time.Sleep(5 * time.Second)
close(done)

for _, t := range ts {
    t.Stop()
}

Try this one on the Go Playground.

icza
  • 389,944
  • 63
  • 907
  • 827
  • All good and fine. But in my case, I have a list of n items, where each item is a function-interval pair. For each item, I want to spin up a goroutine calling the function at its interval. – Chidi Williams Sep 18 '18 at 08:19
  • @ChidiWilliams So call this `schedule()` function, passing each of your functions and their intervals, you may use a loop for that. – icza Sep 18 '18 at 08:21
  • Alright thanks. I came up with this: https://play.golang.org/p/HogqmrdYDSk. Could you add a loop over an array like that in your answer, so I can mark it as correct? – Chidi Williams Sep 18 '18 at 09:43
  • Perfect! Thanks. – Chidi Williams Sep 18 '18 at 11:21
  • @icza "in real-life examples if the app continues to run, leaving the tickers unstopped wastes resources (they will continue to use a background goroutine" - isn't that the idea of a ticker? to run forever periodically until the program exits, like cron? And if the ticker's background routine is always kept alive, won't this cause a memory leak/goroutine leak when we use tickers intended to run forever? – scc Jul 27 '21 at 23:53
  • @scc A ticker's lifetime is not necessarily the app's lifetime. If not, you may use `Ticker.Stop()` to clean its resources. If you don't do it, you won't notice anything with a single ticker. If you create thousands or a million tickers and you don't stop them, you'll notice. But obviously it's good practice to stop a ticker once you don't use it, so if you (or someone else) later extends your code, it won't end up leaking resources if your code is put into a loop. – icza Jul 27 '21 at 23:58
  • @scc If you have a long-running ticker, there is no leaking memory. There are (may be) some shared goroutines in the background that coordinate tickers and try to send on their channels when the time comes, but this doesn't include / mean memory leaking. What may be a leak is if you have an (endless) loop creating tickers and not stopping them. Each new ticker adds some task to the background coordinators, and it'll just keep growing. `Stop()` properly cleans that out. – icza Jul 28 '21 at 00:01
  • @icza Was thinking of the situation where a ticker is intended to run while the program itself runs but that may not always be the case of course. When we do want a cron like ticker - or multiple, eg 10 to run a task every hour - is the best option to create the 10 tickers upfront and keep them forever, or should we stop and create a new one (all 10 in our example) on each cycle/every hour so it freeds associated resources? Used for-loops/goroutines/sleep for the same effect and see a slight memory increase every hour so was thinking of replacing with tickers and wondering on the best approach – scc Jul 28 '21 at 00:16
  • @scc If you need a ticker for long time (hours) or even for the lifetime of the app, then don't stop it, create it once and just leave it running. There's nothing wrong with that. There is no memory leak in that. A ticker's memory usage will not grow over time. All I'm saying is stop a ticker once you don't need it. – icza Jul 28 '21 at 06:14