当前位置: 首页 > article >正文

Golang Ticker Reset异常的坑

前言

延迟执行的场景我们通常会使用time.NewTimer(…)来实现,当一些场合可能需要使用timer.Reset(…)方法修改超时时间,这时使用要多注意, 使用不当会导致Reset失败,或是重复执行两次的情况。

复现

下面这段代码我们是希望:"fmt.Println(“timeout:”, time.Now())"只是执行一次,然后就是deadlock的panic:

package main

import (
	"fmt"
	"time"
)

func main() {
	timer := time.NewTimer(1 * time.Second)
	c := make(chan struct{}, 1)
	c <- struct{}{}
	for {
		select {
		case <-c: // 执行一次
			fmt.Println("start:", time.Now())
			time.Sleep(2 * time.Second)
			timer.Reset(5 * time.Second) // 重置前前实际上时间已经到了

		case <-timer.C:
			fmt.Println("timeout:", time.Now()) // 只希望执行一次
		}
	}
}

但是,实际执行结果如下:

[Running] go run "/Users/hyman/go/src/bsc-flow-backend/internal/infra/redis/locker/main/main.go"
start: 2025-01-27 15:48:06.429385 +0800 CST m=+0.000182501
timeout: 2025-01-27 15:48:08.434472 +0800 CST m=+2.005297501
timeout: 2025-01-27 15:48:13.43967 +0800 CST m=+7.010568084
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [select]:
main.main()
	/Users/hyman/go/src/bsc-flow-backend/internal/infra/redis/locker/main/main.go:13 +0x80
exit status 2

[Done] exited with code=1 in 7.469 seconds

"timeout"执行了两次,这并不符合我们的预期。

分析

查看Timer的源码我们可以发现,"C"其实是一个带缓冲为1的chan:

type Timer struct {
	C <-chan Time
	r runtimeTimer
}
// NewTimer creates a new Timer that will send
// the current time on its channel after at least duration d.
func NewTimer(d Duration) *Timer {
	c := make(chan Time, 1)
	t := &Timer{
		C: c,
		r: runtimeTimer{
			when: when(d),
			f:    sendTime,
			arg:  c,
		},
	}
	startTimer(&t.r)
	return t
}

当执行时间到了,就往C发送当前的时间,发送使用select default的方式来防止阻塞泄露的问题:

// sendTime does a non-blocking send of the current time on c.
func sendTime(c any, seq uintptr) {
	select {
	case c.(chan Time) <- Now():
	default:
	}
}

而Reset方法并不会重新创建"C",只是执行restTimer方法重置触发时间:

func (t *Timer) Reset(d Duration) bool {
	if t.r.f == nil {
		panic("time: Reset called on uninitialized Timer")
	}
	w := when(d)
	return resetTimer(&t.r, w)
}

一般情况下Reset后执行的符合预期,但是当Reset前触发时间已经到了,这个时候"C"实际上是已经有数据,所以如果读取timer.C时就会立即执行,再当Reset后的时间到达时,又也会再触发一次,所以就会出现上面的情况"timeout"执行两次。

方案

解决方案就是reset前C有值时,就先读取出来。官方文档有强调使用Reset方法需要在定时器已经Stop或是“C”里已经没数据的情况下:

// For a Timer created with NewTimer, Reset should be invoked only on

// stopped or expired timers with drained channels.

同时也给出了使用姿势:

// if !t.Stop() {

// <-t.C

// }

// t.Reset(d)

所以,代码就可以改为:

package main

import (
	"fmt"
	"time"
)

func main() {
	timer := time.NewTimer(1 * time.Second)
	c := make(chan struct{}, 1)
	c <- struct{}{}
	for {
		select {
		case <-c: // 执行一次
			fmt.Println("start:", time.Now())
			time.Sleep(2 * time.Second)
			if !timer.Stop() { // 返回false表示C里有值
				<-timer.C
			}
			timer.Reset(5 * time.Second) // 重置前前实际上时间已经到了

		case <-timer.C:
			fmt.Println("timeout:", time.Now()) // 只希望执行一次
		}
	}
}

执行结果:

[Running] go run "/Users/hyman/go/src/bsc-flow-backend/internal/infra/redis/locker/main/main.go"
start: 2025-01-27 16:42:36.051956 +0800 CST m=+0.000152459
timeout: 2025-01-27 16:42:43.062077 +0800 CST m=+7.010485959
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [select]:
main.main()
	/Users/hyman/go/src/bsc-flow-backend/internal/infra/redis/locker/main/main.go:13 +0x80
exit status 2

[Done] exited with code=1 in 7.697 seconds

符合预期

结论

timer调用reset前需要先执行stop方法,如果stop返回的false时,就说明定时器已经被触发,需要执行<-timer.C先读出数据

原文地址:https://itart.cn/blogs/2025/practice/ticker-reset-exception.html


http://www.kler.cn/a/522340.html

相关文章:

  • 多头潜在注意力(MLA):让大模型“轻装上阵”的技术革新——从DeepSeek看下一代语言模型的高效之路
  • 记忆力训练day07
  • 【PostgreSQL内核学习 —— (WindowAgg(一))】
  • 编译器gcc/g++ --【Linux基础开发工具】
  • 动手学图神经网络(4):利用图神经网络进行图分类
  • Cursor的简单使用
  • 第一届“启航杯”网络安全挑战赛WP
  • xss总结标签
  • 滑动窗口详解:解决无重复字符的最长子串问题
  • EtherCAT主站IGH-- 17 -- IGH之fsm_master.h/c文件解析
  • 分布式系统相关面试题收集
  • C语言中宏(Macro)的高级用法:中英双语
  • 人工智能在计算机视觉中的应用与创新发展研究
  • Day27-【13003】短文,什么是栈?栈为何用在递归调用中?顺序栈和链式栈是什么?
  • scikit-learn基本功能和示例代码
  • postgresql 9.4.1 普通表,子表,父表的创建与测试
  • 系统设计的
  • JavaScript系列(46)-- WebGL图形编程详解
  • 专为课堂打造:宏碁推出三款全新耐用型 Chromebook
  • 【实用技能】如何借助Excel处理控件Aspose.Cells,使用 C# 锁定 Excel 中的单元格
  • 获取加工视图下所有元素
  • java后端之事务管理
  • 【C++探索之路】STL---string
  • Day27-【13003】短文,单链表应用代码举例
  • 解决MySQL删除/var/lib/mysql下的所有文件后无法启动的问题
  • 未来五年高速线缆市场有望翻3倍!AEC凭借传输距离优势占比将更高