DEV Community

milktea02
milktea02

Posted on

Misunderstanding Go Timer Resets

My teammate shared a snippet of code with me the other day which led to a fun rabbit hole.

TL;DR:

Remember to drain your timer channel before resetting it if the reset is not a result of the timer firing a notification (e.g. you read from the channel)

What is a Go Timer?

As the name implies, a timer waits a duration of time before firing an event (sending a signal to a channel).

Timers can be stopped, and reset. Just like in real life!

Image description

Here's an example:



t := time.NewTimer(10 * time.Second)

fmt.Println("Wait for 10 seconds for the timer to send a signal")
<- t.C
fmt.Println("It's been 10 seconds!)


Enter fullscreen mode Exit fullscreen mode

Of course, you could always just use time.Sleep(10 * time.Second) in the above example. Unlike sleep, you can stop the Timer before the duration has been met.

Example:



t := time.NewTimer(10 * time.Second)

go func() {
   <- t.C
   fmt.Println("It's been 10 seconds!")
}()

fmt.Println("Never mind, don't wait 10 seconds for the timer")
t.Stop()

time.Sleep(15 * time.Second)
fmt.PrintLn("Slept for 15 seconds!")


Enter fullscreen mode Exit fullscreen mode

And just like a kitchen timer, you can reset it:



t := time.NewTimer(10 * time.Second)

for {
    select {
    case <- t.C:
        // If you're feeling spicy you could reset with a random duration
        t.Reset(5 * time.Second)
    default:
        time.Sleep(time.Second)
    }
}


Enter fullscreen mode Exit fullscreen mode

Everything makes sense until it doesn't

So, I thought I had Timers understood until my teammate sent me this snippet of code:



package main

import (
    "fmt"
    "time"
)

func main() {
    signal := make(chan int)
    go func() {
        for {
            signal <- 1 
        }
    }()

    t := time.NewTimer(time.Second)
    for {
        select {
        case <-signal:
            time.Sleep(2 * time.Second)
            t.Reset(time.Second)
            fmt.Print("1")
        case <-t.C:
            fmt.Print("!")
        default:
            fmt.Print(".")
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

Maybe you're an expert in timers, channels, and select blocks and can see the bug here.

At first glance, I would never expect to see ! printed. But here is example output:



$ go run main.go
.111!1!1!1111


Enter fullscreen mode Exit fullscreen mode

Shouldn't the timer reset prevent the ! branch from ever getting executed? Have I been misunderstanding and using reset incorrectly?

Looking at the documentation for Reset, this is near the top:

For a Timer created with NewTimer, Reset should be invoked only on stopped or expired timers with drained channels.

D'oh! Even if the timer has been reset, it doesn't change the fact that it has already fired and so the channel contains an event! And select will pick a branch "randomly" if more than one branch is ready.

And just a few lines down, we are given instructions on how to use reset:

If a program has already received a value from t.C, the timer is known to have expired and the channel drained, so t.Reset can be used directly.
If a program has not yet received a value from t.C, however, the timer must be stopped and—if Stop reports that the timer expired before being stopped—the channel explicitly drained:



if !t.Stop() {
    <-t.C
}
t.Reset(d)


Enter fullscreen mode Exit fullscreen mode

Putting it all together, correctly

Since we did not receive anything from the timer channel, we need to check if the timer has already went off, and if so we should drain the channel before resetting it. Changing the snippet as so:



for {
    select {
    case <-signal:
        time.Sleep(2 * time.Second)
        if !t.stop() {
            // drain the channel
            <-t.C
        }
        t.Reset(1 * time.Second)
        fmt.Printf("1")
    case <-t.C:
        fmt.Printf("!")
    default:
        fmt.Print(".")
    }
}


Enter fullscreen mode Exit fullscreen mode

Now, all shall be well!

Example output:



$ go run main.go
.111111111111111111111

Enter fullscreen mode Exit fullscreen mode




Resources:


As part of this little journey, we also discovered running this snippet on a MacBook Pro 2019 (intel), vs Ubuntu on WSL (intel) yielded consistently different outputs which I may or may not write about once I have all the facts straight.

Some things I thought about:

  • MacOS vs Ubuntu? MacOS vs Windows? Is this even relevant?
  • ✨schedulers✨
  • Goroutines are not (os) threads. Repeat after me "goroutines are not (os) threads"

Top comments (0)