lxyscls
lxyscls

Reputation: 329

How to stop a timer correctly?

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?

Upvotes: 4

Views: 18177

Answers (4)

Max
Max

Reputation: 15955

Another way to handle a stopped without an independent cancellation signal is to use range on the timer channel. E.g.

timer := time.NewTimer(3 * time.Second)

go func() {
    for range timer.C {
        fmt.Println("I only print if the timer fires")
    }

    fmt.Println("I print after the timer fires or if the timer is stopped")
}()

Upvotes: 0

Ilya  Pikin
Ilya Pikin

Reputation: 181

Have made my own implementation, with good old callbacks and race conditions protection:

import (
    "sync"
    "time"
)

type Timer struct {
    mutex     sync.Mutex
    timer     *time.Timer
    cancel    chan struct{}
    cancelled bool
    completed bool
}

func NewTimer(duration time.Duration, complete func()) *Timer {
    t := &Timer{}
    t.timer = time.NewTimer(duration)
    t.cancel = make(chan struct{})
    go t.wait(complete, func() {})
    return t
}

func NewTimerWithCancel(duration time.Duration, complete func(), cancel func()) *Timer {
    t := &Timer{}
    t.timer = time.NewTimer(duration)
    t.cancel = make(chan struct{})
    go t.wait(complete, cancel)
    return t
}

func (t *Timer) Cancel() {
    t.mutex.Lock()
    if t.completed {
        t.mutex.Unlock()
        return
    }
    t.cancelled = true
    t.mutex.Unlock()
    t.timer.Stop()
    t.cancel <- struct{}{}
}

func (t *Timer) wait(complete func(), cancel func()) {
    for {
        select {
        case <-t.timer.C:
            t.mutex.Lock()
            if !t.cancelled {
                t.completed = true
                t.mutex.Unlock()
                complete()
                return
            }
            t.mutex.Unlock()
        case <-t.cancel:
            cancel()
            return
        }
    }
}

func test() {
    t := NewTimerWithCancel(time.Second, func() {
        fmt.Print("Completed!")
    }, func() {
        fmt.Print("Cancelled!")
    })
    ...
    t.Cancel()
}

Upvotes: 0

Not_a_Golfer
Not_a_Golfer

Reputation: 49187

Adding to the above answer, if you want to cancel all waiters at once, you can encapsulate the behavior using your own timer mechanism that can be cancelled, that sends true or false in an After channel to tell you whether you are waking from a cancellation or a time out, for all waiters.

package main

import (
    "fmt"
    "time"
)

type CancellableTimer struct {
    cancel chan bool
}

func NewCancellableTimer() *CancellableTimer {
    return &CancellableTimer{
        cancel: make(chan bool),
    }
}

// internal wait goroutine wrapping time.After
func (c *CancellableTimer) wait(d time.Duration, ch chan bool) {
    select {
    case <-time.After(d):
        ch <- true
    case <-c.cancel:
        ch <- false
    }
}

// After mimics time.After but returns bool to signify whether we timed out or cancelled
func (c *CancellableTimer) After(d time.Duration) chan bool {
    ch := make(chan bool)
    go c.wait(d, ch)
    return ch
}

// Cancel makes all the waiters receive false
func (c *CancellableTimer) Cancel() {
    close(c.cancel)

}

// a goroutine waiting for cancellation
func B(t *CancellableTimer) {
    select {
    // timedOut will signify a timeout or cancellation
    case timedOut := <-t.After(time.Second):
        if timedOut {
            fmt.Println("Time out!")
        } else {
            fmt.Println("Cancelled!")
        }
    }
}

func main() {
    t := NewCancellableTimer()
    // Start 3 goroutines that wait for different timeouts on the same timer
    go B(t)
    go B(t)
    go B(t)

    // sleep a bit before cancelling
    time.Sleep(100 * time.Millisecond)

    // cancel the timer and all its waiters
    t.Cancel()

    // this is just to collect the output
    time.Sleep(time.Second)

}

Output:

Cancelled!
Cancelled!
Cancelled!

playground link:

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

Upvotes: 4

Peter
Peter

Reputation: 31691

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
    }
}

Upvotes: 9

Related Questions