gothrottling

A time based function throttler in Golang


My use case is: when user stops typing in the text box, I will call a function process_input(), I can't call it on every character, I need to only call it after the typing is finished.

So the idea is: use a helper function to throttle the process_input(), if it is called repeatedly within a period of time, the previous calls will be discarded, only the last call will be triggered after a while.

I want to use it like:


func process_input() {
    fmt.Println(`process_input() called`)
}

func main() {
    fn := buffer.Timed(time.Second, process_input)
    fn() // shouldn't be called
    fn() // may be called or not
    time.Sleep(time.Second)
    fn() // shouldn't be called
    fn() // will be called

    time.Sleep(time.Hour) // block it from exiting
}

My implementation:

package buffer

import (
    "sync/atomic"
    "time"
)

func Timed(dur time.Duration, fn func()) func() {

    kill := make(chan bool)

    isScheduled := atomic.Bool{}

    return func() {
        if isScheduled.Load() { // if there is previous job scheduled
            kill <- true // kill the previous, sometimes this line causes deadlock.
        }

        // start the next
        isScheduled.Store(true)

        go func() {
            defer isScheduled.Store(false)

            select {
            case <-time.After(dur):
                go fn()
            case <-kill:
            }
        }()
    }
}

Sometimes it crashes with deadlock at the line kill <- true, how to fix it?


Solution

  • You've made a number of logical errors dealing with synchronization, but you can avoid most of the complications by reducing the concurrency and just using a timer to trigger the function for you.

    Your goal (at least as it appears in the code above) is to have the function triggered after a delay, and reset that delay on each new call if the function has not yet been called. You can do this with a single time.Timer and a mutex to protect the field: https://go.dev/play/p/54QMJ7xHTZ5

    func NewDebouncer(dur time.Duration) func(fn func()) {
        d := &debouncer{
            dur: dur,
        }
    
        return func(fn func()) {
            d.reset(fn)
        }
    }
    
    type debouncer struct {
        mu    sync.Mutex
        dur   time.Duration
        delay *time.Timer
    }
    
    func (d *debouncer) reset(fn func()) {
        d.mu.Lock()
        defer d.mu.Unlock()
    
        if d.delay != nil {
            d.delay.Stop()
        }
    
        d.delay = time.AfterFunc(d.dur, fn)
    }
    

    An example with multiple calls would look like:

    process_input := func(i int) func() {
        return func() {
            fmt.Printf("process_input() %d called\n", i)
        }
    }
    
    fn := NewDebouncer(time.Second)
    fn(process_input(0))
    fn(process_input(1))
    fn(process_input(2))
    fn(process_input(3))
    
    time.Sleep(1100 * time.Millisecond)
    fn(process_input(4))
    fn(process_input(5))
    fn(process_input(6))
    
    // output:
    // process_input() 3 called
    // process_input() 6 called