Golang中正确重启time.Timer的方法

Golang中正确重启time.Timer的方法 你好!我在代码中使用了 time.Timer。有一个 goroutine 正在等待计时器到期。我还有另一个 goroutine,它希望在特定条件下重启上述计时器。重启计时器时,计时器可能已经到期,也可能尚未到期。第一个 goroutine(正在等待计时器到期)在到期时也会重启计时器。有什么有效的方法可以做到这一点,同时避免任何竞态条件或死锁吗?

顺便说一下,我确实尝试了文档中解释的方法:

if !t.Stop() {
    <-t.C
}
t.Reset(d)

但我观察到在某些情况下,第2行(清空通道)会阻塞。

谢谢

我也在 golang subreddit 上发布了这个问题:https://www.reddit.com/r/golang/comments/f16kqy/the_right_way_to_restart_timetimer/


更多关于Golang中正确重启time.Timer的方法的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

谢谢,这个方法有效。

更多关于Golang中正确重启time.Timer的方法的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


Stop() 的返回值仅指示计时器是否已到期,而不表示是否已有其他协程从通道中读取。在你的情况下,无法确定其他协程是否已经读取过。我会尝试非阻塞读取:

t.Stop()
select {
  case <-t.C:
  default:
}
t.Reset(d)

在 Go 中正确重启 time.Timer 确实需要仔细处理竞态条件。你提到的文档方法在并发场景下确实可能阻塞,因为 Stop() 返回 false 时,定时器可能已经触发但通道尚未被读取,或者正在触发过程中。

以下是两种可靠的重启方法:

方法1:使用独立的停止通道和 select

type RestartableTimer struct {
    timer    *time.Timer
    duration time.Duration
    stopChan chan struct{}
    resetChan chan time.Duration
}

func NewRestartableTimer(d time.Duration) *RestartableTimer {
    rt := &RestartableTimer{
        duration: d,
        stopChan: make(chan struct{}),
        resetChan: make(chan time.Duration),
    }
    rt.timer = time.NewTimer(d)
    go rt.run()
    return rt
}

func (rt *RestartableTimer) run() {
    for {
        select {
        case <-rt.timer.C:
            // 定时器到期,执行操作
            fmt.Println("Timer expired")
            // 可以在这里重启或执行其他逻辑
        case d := <-rt.resetChan:
            if !rt.timer.Stop() {
                select {
                case <-rt.timer.C:
                default:
                }
            }
            rt.timer.Reset(d)
        case <-rt.stopChan:
            if !rt.timer.Stop() {
                select {
                case <-rt.timer.C:
                default:
                }
            }
            return
        }
    }
}

func (rt *RestartableTimer) Reset(d time.Duration) {
    rt.resetChan <- d
}

func (rt *RestartableTimer) Stop() {
    close(rt.stopChan)
}

方法2:使用 sync.Mutex 和重新创建定时器

type SafeTimer struct {
    mu       sync.Mutex
    timer    *time.Timer
    duration time.Duration
    stopped  bool
}

func NewSafeTimer(d time.Duration) *SafeTimer {
    return &SafeTimer{
        timer:    time.NewTimer(d),
        duration: d,
    }
}

func (st *SafeTimer) Reset(d time.Duration) {
    st.mu.Lock()
    defer st.mu.Unlock()
    
    if st.stopped {
        return
    }
    
    // 停止现有定时器
    if !st.timer.Stop() {
        // 非阻塞地清空通道
        select {
        case <-st.timer.C:
        default:
        }
    }
    
    // 重置定时器
    st.timer.Reset(d)
    st.duration = d
}

func (st *SafeTimer) Wait() <-chan time.Time {
    st.mu.Lock()
    defer st.mu.Unlock()
    return st.timer.C
}

func (st *SafeTimer) Stop() {
    st.mu.Lock()
    defer st.mu.Unlock()
    
    st.stopped = true
    if !st.timer.Stop() {
        select {
        case <-st.timer.C:
        default:
        }
    }
}

方法3:使用 time.AfterFunc(推荐)

对于需要频繁重启的场景,time.AfterFunc 通常更简单安全:

type RestartableTimer struct {
    mu       sync.Mutex
    timer    *time.Timer
    callback func()
}

func NewRestartableTimer(d time.Duration, callback func()) *RestartableTimer {
    rt := &RestartableTimer{callback: callback}
    rt.timer = time.AfterFunc(d, rt.execute)
    return rt
}

func (rt *RestartableTimer) execute() {
    rt.mu.Lock()
    defer rt.mu.Unlock()
    
    // 执行回调
    rt.callback()
}

func (rt *RestartableTimer) Reset(d time.Duration) {
    rt.mu.Lock()
    defer rt.mu.Unlock()
    
    // Stop 总是安全的,即使定时器已经触发
    rt.timer.Stop()
    rt.timer = time.AfterFunc(d, rt.execute)
}

func (rt *RestartableTimer) Stop() {
    rt.mu.Lock()
    defer rt.mu.Unlock()
    rt.timer.Stop()
}

使用示例

func main() {
    // 使用方法3
    timer := NewRestartableTimer(2*time.Second, func() {
        fmt.Println("Timer expired at", time.Now())
    })
    
    // 在另一个 goroutine 中重启定时器
    go func() {
        time.Sleep(1 * time.Second)
        timer.Reset(3 * time.Second)
    }()
    
    // 等待足够时间观察结果
    time.Sleep(5 * time.Second)
    timer.Stop()
}

关键点:

  1. 使用互斥锁保护定时器状态
  2. 清空通道时使用 select 避免阻塞
  3. 考虑使用 time.AfterFunc 简化逻辑
  4. 确保定时器停止后不再被使用

这些方法都能避免你遇到的阻塞问题,并安全处理并发重启。

回到顶部