Golang debounce 防抖动

今天在看某个开源项目源码的时候发现了一个有意思的代码段

1
2
3
4
5
6
7
8
9
err = watchFile(tunnelsConfPath, servicesConfPath, func(event fsnotify.Event) {
manager.notify()
})
// ...
func (m *Manager) notify() {
m.debounce(func() {
m.events <- struct{}{}
})
}

原理分析

这个 debounce 的作用是,对于如下场景,在 1s 内用户更新了 100 次文件内容,希望程序只根据最后一次更新的文件内容刷新配置,以减小程序的压力。

debounce 实现的主要思想是:延迟执行相应的函数。比如说,t0 时需要执行函数,那么就会被延迟到 t0 + 500ms 执行,如果在此期间函数又被执行,比如 t1,则取消之前需要在 t0 + 500ms 执行的定时器,变为 t1 + 500ms 时执行 ,简单的实现如下:

1
2
3
4
5
6
7
8
9
func NewDebounce(after time.Duration) func(f func()) {
var timer *time.Timer
return func(f func()) {
if timer != nil {
timer.Stop()
}
timer = time.AfterFunc(after, f)
}
}

编写如下函数进行测试。虽然内部循环执行了 1000 次,但是 f 只会被执行一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
d := NewDebounce(time.Millisecond * 100)
var data int64
f := func() {
atomic.AddInt64(&data, 1)
}
for i := 0; i < 10; i++ {
for j := 0; j < 1000; j++ {
d(f)
}
time.Sleep(time.Millisecond * 100)
}
fmt.Println(data) // 9
}

开源实现

对于并发环境,debounce 中需要为 timer 加锁,GitHub - bep/debounce: A debouncer written in Go 给出了一个 debounce 实现,代码实现逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func New(after time.Duration) func(f func()) {
d := &debouncer{after: after}
return func(f func()) {
d.add(f)
}
}

type debouncer struct {
mu sync.Mutex
after time.Duration
timer *time.Timer
}

func (d *debouncer) add(f func()) {
d.mu.Lock()
defer d.mu.Unlock()

if d.timer != nil {
d.timer.Stop()
}
d.timer = time.AfterFunc(d.after, f)
}