timer.Timer
is simple but it’s not as straightforward to use it as it looks.
What can we do with time.Timer?
There are two functions to create a new instance of time.Timer
. It’s time.NewTimer
and time.AfterFunc
. Let’s see the difference.
When creating an instance by time.NewTimer
, we have to receive a notification from timer.C
channel to do something after the specified time elapses.
func runTimer1() {
timer := time.NewTimer(time.Second)
var wg sync.WaitGroup
wg.Add(1)
go func(timer *time.Timer) {
<-timer.C
fmt.Println("this is executed after 1 second")
wg.Done()
}(timer)
wg.Wait()
}
When using time.AfterFunc
, we don’t have to use timer.C
. The callback is automatically called after the specified time elapses.
func runTimer2() {
var wg sync.WaitGroup
wg.Add(1)
callback := func() {
fmt.Println("this is executed after 1 second")
wg.Done()
}
timer := time.AfterFunc(time.Second, callback)
wg.Wait()
}
This time.Timer
can be reset and stop.
timer.Stop()
timer.Reset()
It looks so simple but we have to be careful when using Stop/Reset.
How to Stop time.Timer correctly
When calling Stop()
method, the timer could be already expired because the timer is running concurrently. We can know whether the timer has expired or not by the return value of Stop()
function.
if timer.Stop() {
// timer stopped
} else {
// timer was already expired
}
It’s better to consume timer.C
if timer.Stop()
returns false to make the subsequent process work properly.
// Don't use this code
if !timer.Stop() {
<-timer.C
}
However, <-timer.C
could block if it is already consumed. To avoid the case, it’s better to write it in the following way.
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
If you don’t need to reset the timer, the select clause is not needed.
How to Reset time.Timer correctly
timer.Reset()
resets the timer. There are two cases when using this function.
- The timer has not been expired yet and needs to be reset and extended the time
- The timer has already expired
The timer must be stopped or has expired when calling timer.Reset()
because it’s not possible to know whether the notification to the channel timer.C
is before or after the function call if timer.Reset()
is called for the running timer.
Therefore, timer.Stop()
must be called first. Even if the timer is not expired when timer.Reset()
is called, the state could change while the reset process is running. timer.Stop()
must always be called.
// NG code
if !timer.Stop() {
<-timer.C: // this blocks when timer has been expired
}
timer.Reset()
However, <-timer.C
could block in a case where the timer has already expired because it’s already consumed. As I mentioned above, it must be written in a select clause.
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
timer.Reset()
Channel timer.C is not assigned when using AfterFunc
I used timer.AfterFunc
. It blocked when I added the stop function call with <-timer.C
. Let’s try one thing.
func runTimer2() {
var wg sync.WaitGroup
wg.Add(1)
callback := func() {
fmt.Println("this is executed after 1 second")
wg.Done()
}
timer := time.AfterFunc(time.Second, callback)
wg.Wait()
fmt.Println(<-timer.C) // Added here
}
The result is as follows.
this is executed after 1 second
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive (nil chan)]:
play-with-go-lang/utils.runTimer2()
/workspaces/play-with-go-lang/utils/timer.go:42 +0xa6
play-with-go-lang/utils.RunTimer(...)
/workspaces/play-with-go-lang/utils/timer.go:11
main.main()
/workspaces/play-with-go-lang/main.go:47 +0x10
exit status 2
I thought timer.C
is already consumed on another goroutine to call the callback but it’s not.
If we check the code in GitHub, we can see the comment below.
// AfterFunc waits for the duration to elapse and then calls f
// in its own goroutine. It returns a Timer that can
// be used to cancel the call using its Stop method.
// The returned Timer's C field is not used and will be nil. <----- this line was added
func AfterFunc(d Duration, f func()) *Timer {
I checked the comment in version 1.21.5 but it is not added.
It means that we don’t have to take care about the channel timer.C
when using time.AfterFunc
.
How to Debounce in Golang
If Debounce is needed, it can be implemented by using time.AfterFunc
. As we know, we don’t have to take care of channel timer.C
. So we just call timer.Stop()
followed by timer.Reset()
.
type Debouncer struct {
timeout time.Duration
timer *time.Timer
callback func()
}
func NewDebounce(timeout time.Duration, callback func()) Debouncer {
return Debouncer{
timeout: timeout,
callback: callback,
}
}
func (m *Debouncer) Debounce() {
if m.timer == nil {
m.timer = time.AfterFunc(m.timeout, m.callback)
return
}
m.timer.Stop()
m.timer.Reset(m.timeout)
}
I think the following code is enough if the callback needs to be updated.
func (m *Debouncer) UpdateDebounceCallback(callback func()) {
m.timer.Stop()
m.timer = time.AfterFunc(m.timeout, callback)
}
If the two functions are called on other goroutines, it must be locked by Mutex. It would be good to know about atomic if you don’t know it.
type Debouncer struct {
timeout time.Duration
timer *time.Timer
callback func()
mutex sync.Mutex // Added
}
func (m *Debouncer) Debounce() {
m.mutex.Lock() // Added
defer m.mutex.Unlock() // Added
if m.timer == nil {
m.timer = time.AfterFunc(m.timeout, m.callback)
return
}
m.timer.Stop()
m.timer.Reset(m.timeout)
}
func (m *Debouncer) UpdateDebounceCallback(callback func()) {
m.mutex.Lock() // Added
defer m.mutex.Unlock() // Added
m.timer.Stop()
m.timer = time.AfterFunc(m.timeout, callback)
}
It can be used in this way below.
func runDebounce() {
var wg sync.WaitGroup
wg.Add(1)
debounceCallCount := 0
callback := func() {
fmt.Printf("Debounce call count: %d\n", debounceCallCount)
wg.Done()
}
debouncer := NewDebounce(time.Second, callback)
debounceCallCount++
debouncer.Debounce()
time.Sleep(500 * time.Millisecond)
debounceCallCount++
debouncer.Debounce()
time.Sleep(500 * time.Millisecond)
debounceCallCount++
debouncer.Debounce()
time.Sleep(500 * time.Millisecond)
wg.Wait()
}
When this function is called, it shows only Debounce call count: 3
. It’s properly debounced.
Overview
There are two ways to instantiate time.Timer
.
time.NewTimer()
time.AfterFunc()
Drain timer.C
before calling timer.Reset()
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
timer.Reset()
When using time.AfterFunc()
, we don’t have to care about the channel timer.C
because it’s nil.
Comments