2

I've written a code snipped that creates a timer with a 0 length time, and it does not immediately expire (which is what I expected). A very short sleep call does make it expire, but I'm confused as to why.

The reason I care is that the code using this idea has a snippet that returns 0 on a low probability error, with the idea that the timer should be set to immediately expire, and retry a function. I do not believe that the nanosecond sleep needed here will affect my implementation, but it bothers me.

Did I make a mistake, is this expected behaviour?

Thanks!

 package main

    import (
        "fmt"
        "time"
    )

    func main() {
        testTimer := time.NewTimer(time.Duration(0) * time.Millisecond)
        fmt.Println(Expired(testTimer))
        time.Sleep(time.Nanosecond)
        fmt.Println(Expired(testTimer))
    }

    func Expired(T *time.Timer) bool {
        select {
        case <-T.C:
            return true
        default:
            return false
        }
    }

Playground link: https://play.golang.org/p/xLLHoR8aKq

Prints

false
true
Qubert
  • 185
  • 3
  • 9

3 Answers3

7

time.NewTimer() does not guarantee maximum wait time. It only guarantees a minimum wait time. Quoting from its doc:

NewTimer creates a new Timer that will send the current time on its channel after at least duration d.

So passing a zero duration to time.NewTimer(), it's not a surprise the returned time.Timer is not "expired" immediately.

The returned timer could be "expired" immediately if the implementation would check if the passed duration is zero, and would send a value on the timer's channel before returning it, but it does not. Instead it starts an internal timer normally as it does for any given duration, which will take care of sending a value on its channel, but only some time in the future.

Note that with multiple CPU cores and with runtime.GOMAXPROCS() being greater than 1 there is a slight chance that another goroutine (internal to the time package) sends a value on the timer's channel before NewTimer() returns, but this is a very small chance... Also since this is implementation detail, a future version might add this "optimization" to check for 0 passed duration, and act as you expected it, but as with all implementation details, don't count on it. Count on what's documented, and expect no more.

icza
  • 389,944
  • 63
  • 907
  • 827
  • That last edit hit the spot. It is a property of the time it takes a go routine to set up the timer object. – RayfenWindspear Apr 25 '17 at 16:55
  • > sends a value on the timer's channel before NewTimer() returns, but this is a very small chance... Are there any negative implications? Won't the timer's channel just block until it's read? – hbogert Aug 26 '21 at 20:29
  • @hbogert Channels don't block, only communication ops can block (e.g. send or receive). And they only block if the channel's buffer is full and the other end of the communication is not ready. Again, it is implementation detail, but the channel of the timer is buffered. So it would be possible that a value already sits in the returned channel's buffer before `NewTimer()` returns. – icza Aug 26 '21 at 21:11
  • Ah only now I get the point you're making, You are arguing that the current implementation non-deterministically (i.e., sometimes) actually does immediately trigger, that is from the perspective of the caller. – hbogert Aug 27 '21 at 06:39
  • @hbogert Sending a value on the returned channel happens in another goroutine, not on the one that calls `NewTimer()`. So when and if that happens depends on the goroutine scheduler. It's not hard-coded in `NewTimer()` to check if the duration is zero and send a value on the channel if so, so there's absolutely no guarantee that a value is sent before `NewTimer()` returns. It's much more likely that it won't be. – icza Aug 27 '21 at 06:44
2

Go's timer functions guarantee to sleep at least the specified time. See the docs for Sleep and NewTimer respectively:

Sleep pauses the current goroutine for at least the duration d. A negative or zero duration causes Sleep to return immediately.

NewTimer creates a new Timer that will send the current time on its channel after at least duration d.

(emphasis added)

In your situation, you should probably just not use a timer in the situation that you don't want to sleep at all.

Community
  • 1
  • 1
Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
0

This is due to the internal time it takes to set up the timer object. If you'll note in the playground link below the timer does expire at the proper time, but the internal go routine that sets it up and starts it takes longer than your Expire function does to check it.

When the Timer expires, the current time will be sent on C (the channel)

So you'll notice that after it expires, it still sends the original time, because it has expired even before the nanosecond Sleep finished.

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

package main

import (
    "fmt"
    "time"
)

func main() {
    testTimer := time.NewTimer(0 * time.Millisecond)
    Expired(testTimer)
    time.Sleep(time.Nanosecond)
    Expired(testTimer)
    n := time.Now()
    fmt.Printf("after waiting: %d\n", n.UnixNano())

}

func Expired(T *time.Timer) bool {
    select {
    case t:= <-T.C:
        fmt.Printf("expired %d\n", t.UnixNano())
        return true
    default:
        n := time.Now()
        fmt.Printf("not expired: %d\n", n.UnixNano())
        return false
    }
}
RayfenWindspear
  • 6,116
  • 1
  • 30
  • 42