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?
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