How to stop a timer correctly?

Issue

var timer *time.Timer

func A() {
    timer.Stop() // cancel old timer
    go B() // new timer
}

func B() {
    timer = time.NewTimer(100 * time.Millisecond)
    select {
    case <- timer.C:
    // do something for timeout, like change state
    }
}

Function A and B are all in different goroutines.

Say A is in a RPC goroutine. When application receives RPC request, it will cancel the old timer in B, and start a new timer in another goroutine.

The doc say:

Stop does not close the channel, to prevent a read from the channel
succeeding incorrectly.

So how to break the select in B to avoid goroutine leak?

Solution

Use an additional, independent cancellation signal. Since you already have a select statement in place, another channel is an obvious choice:

import "time"

var timer *time.Timer
var canceled = make(chan struct{})

func A() {
    // cancel all current Bs
    select {
    case canceled <- struct{}{}:
    default:
    }   

    timer.Stop()

    go B()       // new timer
}

func B() {
    timer = time.NewTimer(100 * time.Millisecond)
    select {
    case <-timer.C:
        // do something for timeout, like change state
    case <-canceled:
        // timer aborted
    }
}

Note that all As and Bs race against each other for the timer value. With the code above it is not necessary to have A stop the timer, so you don’t need a global timer, eliminating the race:

import "time"

var canceled = make(chan struct{})

func A() {
    // cancel all current Bs
    select {
    case canceled <- struct{}{}:
    default:
    }

    go B()
}

func B() {
    select {
    case <-time.After(100 * time.Millisecond):
        // do something for timeout, like change state
    case <-canceled:
        // aborted
    }
}

Answered By – Peter

Answer Checked By – Cary Denson (GoLangFix Admin)

Leave a Reply

Your email address will not be published.