Cedric Fung

Mixin me 25566

Release Golang Timer

Jul 07, 2020

About 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.

About the Author

Core developer of Mixin Network. Passionate about security and privacy. Strive to formulate elegant code, simple design and friendly machine.

25566 @ Mixin Messenger

https://vec.io