Release Golang Timer
Jul 07, 2020About 4 months ago, I was reading the go doc of time
package and found that I had used time.After
incorrectly for years.
If efficiency is a concern, use NewTimer instead and call Timer.Stop if the timer is no longer needed.
I often use Timer
as a timeout mechanism to wait some other channel data, that means I have created millions of Timer
object which should have been stopped without stale in the memory. So I quickly made some modifications on all similar usage.
// before
for {
select {
case <-other:
case <-time.After(period):
}
}
// after
for {
timer := time.NewTimer(period)
select {
case <-other:
case <-timer.C:
}
timer.Stop()
}
I felt good that I fixed some potential performance issue, and all seemed to follow the guides of the doc, use NewTimer
and call Timer.Stop
if no longer needed. And I use this trick in all recent code. Until last month, someone submitted an issue regarding my Timer
usage, suggested on using Timer.Reset
to reduce allocations and improve performance. After some discussions we finally made the changes and benchmarks were good.
timer := time.NewTimer(period)
for {
if !timer.Stop() {
<-timer.C
}
timer.Reset(period)
select {
case <-other:
case <-timer.C:
}
}
It works with this modification in production for days, then I thought I should do this to all my Timer
code and made all similar changes to almost all my code. And when I ran the tests, weird things happened, the program blocked at timer.Reset
. So I read the doc again.
Reset should be invoked only on stopped or expired timers with drained channels. If a program has already received a value from t.C, the timer is known to have expired and the channel drained, so t.Reset can be used directly. If a program has not yet received a value from t.C, however, the timer must be stopped and—if Stop reports that the timer expired before being stopped—the channel explicitly drained.
Very confusing, but I finally understood the block happens because period
elapsed, i.e. timer.C
has been drained. At this time Stop()
would return false because the timer has already expired, then the second <-timer.C
would block forever. I haven’t figured out a neat way to release the timer yet, I only add another variable to indicate whether the channel has been drained.
drained := false
timer := time.NewTimer(period)
for {
if !drained && !timer.Stop() {
<-timer.C
}
drained = false
timer.Reset(period)
select {
case <-other:
case <-timer.C:
drained = true
}
}
So far so good.