20210121timer和ticker啃了3天(1) - ziyouzy/2021blog GitHub Wiki
golang的timer和ticker使用起来要顾虑很多,主要是如何才能最靠谱的Stop,最靠谱的到期,最靠谱的Reset,所担心的问题这篇文章里做了描述:
http://russellluo.com/2018/09/the-correct-way-to-use-timer-in-golang.html
有三个问题:
1.按照 Timer.Stop 文档 的说法
每次调用 Stop 后需要判断返回值
如果返回 false(表示 Stop 失败,Timer 已经在 Stop 前到期)
则需要排掉(drain)channel 中的事件:
if !t.Stop() {
<-t.C
}
2.如果之前程序已经从 channel 中接收过事件
那么上述 <-t.C 就会发生阻塞。
可能的解决办法是借助 select 进行非阻塞排放(draining):
if !t.Stop() {
select {
case <-t.C: // try to drain the channel
default:
}
}
3.因为 channel 的发送和接收发生在不同的 goroutine
所以存在竞争条件(race condition)
最终可能导致 channel 中的事件未被排掉
1和2两个问题和解决方案我都已经理解了,现在把第三个啃下来:
发送线程(生产者)是time包内置结构类的内置goroutine,而接收(消费者)则是在使用者自己的线程
也就是说没有你想象的复杂,只是这两者之间的竞争条件,对于纯粹的竞争条件,请先看如下文章,解释的很透彻
https://zhuanlan.zhihu.com/p/201377528?utm_source=wechat_session
而回到当前的情况,竞争条件发生于timer.C这个内置字段,到期对其的添加数据操作,以及程序员对其的取出数据操作
不用说的那么抽象,其实3就是在总结1和2:
当执行1所示代码前管道内无数据则出现阻塞bug
当执行2所示代码后管道内有数据则出现数据未排除bug
这些都是“竞争条件”造成的问题
如:
goroutine A:Go 运行时判断 Timer 已经到期,于是从最小堆中删除该 Timer
goroutine B:应用程序执行 Timer.Stop,发现 Timer 已经到期,进而返回 false
goroutine B:应用程序继续执行 select...case <-t.C,因为 channel 中并没有事件,所以会立即返回
goroutine A:Go 运行时将到期事件发送到该 Timer 的 channel 中
这里有个只是点,那就是timer到期后的第一件事事从最小堆删除该Timer
之前总是在思考到期后timer结构类的Stop函数究竟做了什么,其实就是在最小堆中“删除自己”
但是在删除之前,必做的一件事是往timer.C中写入一个事件
(从最小堆删除自己是最后一步,1不会妨碍此操作)然后在到期所导致的写入事件执行后,需要话一瞬间的时间完成写入这个操作,如果在这一瞬间里执行了stop则会返回false触发1所示处理stop==false的逻辑!且在这个逻辑执行完后才写入数据!,整体局面就变成了一次写入,两次读取,这就是1的情况
(从最小堆删除自己是最后一步,2会妨碍此操作)然而同样的在这一瞬间,第二种处理stop=false的代码在录入操作前就执行结束了,触发2所示处理stop==flase的逻辑!且在这个逻辑执行完后才写入数据!,就会起不到任何作用了
也就是说,1和2都在尝试解决同一个问题
只不过1会造成程序或者某个携程死锁的bug因此不可取(会让程序员自己写的作用域无法销毁)
而2虽然不会造成1的bug但是管道内会有未读取的数据(会让timer结构类的作用与无法销毁)