4

I have some function that has to run periodically. I have used a ticker for this. But if the ticker is already running, and the time interval passes again, it should not execute again.

package main

import (
    "fmt"
    "time"
)

func main() {
    ticker := time.NewTicker(3*time.Second)
    flag := 0
    defer ticker.Stop()
    for {
        select {
        case t := <-ticker.C:
            flag = flag + 1
            if (flag % 2 ==0 ) {
                time.Sleep(time.Second*4)
            }   
            fmt.Println("Current time: ", t)
        }
    }
}

https://play.golang.org/p/2xV2MYInn4I

In the playground, the ticker prints every 3 seconds, but every even turn of the ticker the job takes more time than the interval. I expect it to not run then and drop those ticks.

How do I do this?

Scott Stensland
  • 26,870
  • 12
  • 93
  • 104
leoOrion
  • 1,833
  • 2
  • 26
  • 52
  • "I expect it to not run then and drop those ticks." Your expectation is wrong. The ticker ticks evenly spaced and doesn't know what your code does or does not. You can either "drop" this ticks yourself or not use a time.Ticker but e.g. just time.Sleep how long you want to wait before the next event, taking account of how long your task took. – Volker Feb 19 '20 at 10:42
  • `drop those ticks myself`. How do I figure out if my ticker is currently running in order to do this? – leoOrion Feb 19 '20 at 10:47
  • "if my ticker is currently running" You cannot, that is not how tickers work, your ticker is not "running" (well, it is running until the end of the program). Your _code_ is running or not running. You do not want your code to run when its last finish time is later than the next tick. So remember when your code finished the last time, compare that to the tickers timestamp and rerun your code or not. – Volker Feb 19 '20 at 10:53
  • Will try that. Is there any other way to achieve this instead of a ticker? – leoOrion Feb 19 '20 at 11:01
  • Yes, decide when you want to run your code the next time and just sleep that long. – Volker Feb 19 '20 at 11:29

2 Answers2

5

The ticker channel is buffered, which is why you may see multiple triggers right one after the other. You can prevent that by simply transfering the ticker's values to an unbuffered channel (note also that the time.Time value received from the ticker is not the current time but the time of the last tick):

package main

import (
    "fmt"
    "time"
)

func main() {
    c := make(chan time.Time) // unbuffered
    ticker := time.NewTicker(3 * time.Second)
    defer ticker.Stop()

    go func() {
        for t := range ticker.C {
            select {
            case c <- t:
            default:
            }
        }
    }()

    for flag := 0; flag < 8; flag++ {
        <-c

        if flag%2 == 0 {
            time.Sleep(time.Second * 4)
        }
        fmt.Println("Current time: ", time.Now())
    }
}

// Output:
// Current time:  2020-02-19 12:21:57.095433032 +0100 CET m=+3.000213350
// Current time:  2020-02-19 12:22:04.095585208 +0100 CET m=+10.000365520
// Current time:  2020-02-19 12:22:06.095363327 +0100 CET m=+12.000143680
// Current time:  2020-02-19 12:22:13.095605268 +0100 CET m=+19.000385598
// Current time:  2020-02-19 12:22:15.095371885 +0100 CET m=+21.000152174
// Current time:  2020-02-19 12:22:22.095537562 +0100 CET m=+28.000317857
// Current time:  2020-02-19 12:22:24.095431317 +0100 CET m=+30.000211625
// Current time:  2020-02-19 12:22:31.095524308 +0100 CET m=+37.000304595

Try it on the playground: https://play.golang.org/p/jDe5uJiRVe2

Peter
  • 29,454
  • 5
  • 48
  • 60
4

sleeping inside the same goroutine merely delays execution. ticker meanwhile runs in a separate goroutine. So even if you used a global variable to maintain an execution state - it will not give you your desired result with sleep. However migrating the whole "sleeping" in a separate goroutine yields:

package main

import (
    "fmt"
    "time"
)

type Tick struct {
    ticker *time.Ticker
    executing bool
}

func somethingYouWantToDo(tick *Tick, flag *int, t time.Time) {
    if tick.executing {
        return
    }

    tick.executing = true

    *flag = *flag + 1

    if (*flag % 2 ==0 ) {
                time.Sleep(time.Second*4)
    }   
        fmt.Println("Current time: ", t)
    tick.executing = false
}

func main() {
    tick := &Tick{
        ticker: time.NewTicker(3*time.Second),
    }
    flag := 0
    defer tick.ticker.Stop()
    for {
        select {
        case t := <-tick.ticker.C:
            go somethingYouWantToDo(tick, &flag, t)
        }
    }
}
// output
// Current time:  2009-11-10 23:00:03 +0000 UTC m=+3.000000001
// Current time:  2009-11-10 23:00:06 +0000 UTC m=+6.000000001
// Current time:  2009-11-10 23:00:12 +0000 UTC m=+12.000000001
// Current time:  2009-11-10 23:00:15 +0000 UTC m=+15.000000001
// Current time:  2009-11-10 23:00:21 +0000 UTC m=+21.000000001
// Current time:  2009-11-10 23:00:24 +0000 UTC m=+24.000000001
// Current time:  2009-11-10 23:00:30 +0000 UTC m=+30.000000001
// Current time:  2009-11-10 23:00:33 +0000 UTC m=+33.000000001
// Current time:  2009-11-10 23:00:39 +0000 UTC m=+39.000000001
// Current time:  2009-11-10 23:00:42 +0000 UTC m=+42.000000001
// Current time:  2009-11-10 23:00:48 +0000 UTC m=+48.000000001
// Current time:  2009-11-10 23:00:51 +0000 UTC m=+51.000000001
// Current time:  2009-11-10 23:00:57 +0000 UTC m=+57.000000001
// Current time:  2009-11-10 23:01:00 +0000 UTC m=+60.000000001
// Current time:  2009-11-10 23:01:06 +0000 UTC m=+66.000000001
// Current time:  2009-11-10 23:01:09 +0000 UTC m=+69.000000001

Try it on the playground

Kelsnare
  • 635
  • 5
  • 12
  • Yeah. Finally figured it out myself. Similar concept. https://play.golang.org/p/g6qwQ8BcHDE – leoOrion Feb 19 '20 at 13:00
  • Note that you now have a data race: the first goroutine that inspects the flag, and clears it, is racing against any other goroutine you start that will inspect the flag and clear it afterward. Because the ticks are very far apart (seconds), it's very unlikely that this race will cause problems, but it still exists. – torek Feb 19 '20 at 22:17
  • Which one will? mine -> play.golang.org/p/g6qwQ8BcHDE or the one in the answer. In my case, at any point in time only one goroutine will run. – leoOrion Feb 20 '20 at 02:13
  • This example is pretty dangerous for newcomers.. Please consider changing executing type with a Mutex or a buffered channel! https://stackoverflow.com/a/52882045/3673430 – tuxErrante May 04 '23 at 12:23