3
votes
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?

3
Please describe the high level problem you are trying to solve. - Dima Tisnek
Can you elaborate your question? - Shahriar
@aerokite When timeout, switch to next state. when receive request, keep the state and start a new timer. - lxyscls
@lxyscls what real problem do you have with this code though? The 100ms timer is not stopped. So what? - zerkms
Just use a cancellation channel and select on both - Not_a_Golfer

3 Answers

6
votes

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

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

0
votes

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