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结构类的作用与无法销毁)

总结,其实timer内部的C字段没什么神秘的,到期后会先执行向C的写入操作,然后再销毁自身,而对于这个C,你如果不去读取那么就会阻塞在写入他的那一行他和包裹他的携程函数以及所在的携程就会一直停留在那,造成内存泄漏

如果这个C管道本身没有数据了,但是你自己的代码还尝试读取他,也会阻塞在读取他的那一行,他和你自己写的包裹他的携程函数以及所在的携程就会一直停留在那,造成内存泄漏

1和2都在解决同一个问题,只不过解决方式都不完美,这一切都是竞争条件所造成的问题