Reputation: 329
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
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
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
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
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