上篇文章写了关于context 源码解读,里面涉及到不少的计时器,所以我们这篇文章就简单了解下go的计时器。Go的计时器主要Timer
和Ticker两种,下面我们开始一起学习下
go的计时器基于Go
运行时计时器runtime.timer
实现的,rumtime.timer
的结构体表示如下
type runtimeTimer struct {
pp uintptr
when int64
period int64
f func(interface{}, uintptr) // NOTE: must not be closure
arg interface{}
seq uintptr
nextwhen int64
status uint32
}
p p
—地址
when
— 当前计时器被唤醒的时间;
period
— 两次被唤醒的间隔;
f
— 每当计时器被唤醒时都会调用的函数;
arg
— 计时器被唤醒时调用 f
传入的参数;
nextWhen
— 计时器处于 timerModifiedLater/timerModifiedEairlier
状态时,用于设置 when
字段;
status
— 计时器的状态;
type Timer struct {
C <-chan Time // 单向chan,将时间写入chan
r runtimeTimer
}
实例
t := time.NewTimer(time.Second * 2)
defer t.Stop()
for {
<-t.C
fmt.Println("timer running...")
// 重置Reset 使 t 重新开始计时
t.Reset(time.Second * 2)
}
源码
// NewTimer创建一个新计时器,该计时器将在时间段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
}
func sendTime(c interface{}, seq uintptr) {
select {
case c.(chan Time) <- Now():
default:
}
}
sendTime
将当前时间发送到Timer
的时间channel
中(NewTimer
创建的一个带缓冲的channel)。
Timer.C
这个channel
有没有接收方sendTime
都可以非阻塞的将当前时间发送给Timer.C
,而且sendTime
中还加了双保险:通过select
判断Timer.C
的Buffer
是否已满,一旦满了会直接退出,不会阻塞。
实例
//⭐️这个例子有问题
mychan := make(chan int)
go func() {
fmt.Println("wait 4's")
time.Sleep(time.Second * 4)
mychan <- 1
}()
for{
select {
case <-mychan:
fmt.Println("结束了")
// 拿掉return, 就会出现问题
return
case <-time.After(time.Second * 1):
fmt.Println("wait 1's ")
}
}
源码
//在等待持续时间过后,然后在返回的通道上发送当前时间。
//它相当于NewTimer方法。在计时器触发之前,垃圾收集器不会恢复基础计时器。
//如果效率是一个问题,请改用新定时器并调用定时器。如果不再需要定时器,请停止。
func After(d Duration) <-chan Time {
return NewTimer(d).C
}
内存泄漏问题分析
首先我们从源码看出来,after是创建新的Timer对象的,然后我们如果把return拿掉,那么这个代码后续会一直走after,不停的创建新的对象。所以我们以后如果在for-select的情况下我们可以考虑使用其他的,如果是单独使用记得加上defer 来停止任务。
实例
mychan := make(chan int)
time.AfterFunc(6*time.Second, func() {
fmt.Println("you must wait 6's ")
mychan <- 1
})
for {
select {
case <-mychan:
fmt.Println("Game Over")
return
default:
fmt.Println("wait 3's")
time.Sleep(3 * time.Second)
}
}
源码
//AfterFunc等待d 的时间段过去,然后在自己的goroutine中调用函数f。它返回一个计时器,能够用于使用其Stop方法取消调用。
func AfterFunc(d Duration, f func()) *Timer {
t := &Timer{
r: runtimeTimer{
when: when(d),
f: goFunc,
arg: f,
},
}
startTimer(&t.r)
return t
}
从上面源码可以看到外面传入的f
参数并非直接赋值给了运行时计时器的f
,而是作为包装函数goFunc
的参数传入的。goFunc
会启动了一个新的goroutine
来执行外部传入的函数f
。这是因为所有计时器的事件函数都是由Go
运行时内唯一的goroutine
timerproc
运行的。为了不阻塞timerproc
的执行,必须启动一个新的goroutine
执行到期的事件函数。
源码
//停止计时器。如果调用停止计时器,则返回true;如果计时器已过期或已停止,则返回false。
// Stop不会关闭通道,以防止通道读取错误。对于使用AfterFunc(d,f)创建的计时器,
// 如果t.Stop返回false,则计时器已过期,并且函数f已在其自己的goroutine中启动;
// Stop不会在返回前等待f完成。如果调用方需要知道f是否已完成,则必须显式地与f协调
func (t *Timer) Stop() bool {
if t.r.f == nil {
panic("time: Stop called on uninitialized Timer")
}
return stopTimer(&t.r)
}
源码
//Reset会将计时器更改为在d时间段后过期。如果计时器处于active,则返回true;
//如果计时器已过期或已停止,则返回false。我们应该在已停止或过期且chan为空的计时器上调用Reset。
//如果一个程序已经从t.C.接收到一个值,则已知定时器已过期,通道已耗尽,因此可以直接使用t.Reset。
//重置计时器时必须注意不要与当前计时器到期发送时间到t.C的操作产生竞争。如果程序已经从t.C接收到值,
//则计时器是已知的已过期,并且t.Reset可以直接使用。如果程序尚未从t.C接收值,计时器必须先被停止,
//并且-如果使用t.Stop时报告计时器已过期,那么请排空其通道中值
func (t *Timer) Reset(d Duration) bool {
if t.r.f == nil {
panic("time: Reset called on uninitialized Timer")
}
w := when(d)
active := stopTimer(&t.r)
resetTimer(&t.r, w)
return active
}
time.Reset存在的问题
正常的情况
c := make(chan bool)
go func() {
// 生产
for i := 0; i < 5; i++ {
time.Sleep(time.Second * 1)
c <- false
}
time.Sleep(time.Second * 1)
c <- true
}()
go func() {
// 消费
timer := time.NewTimer(time.Second * 5)
for {
// 如果过期了将,chan消费完
if !timer.Stop() {
<-timer.C
}
timer.Reset(time.Second * 5)
select {
// 没有过期,判断chan里的数据
case b := <-c:
if b == false {
fmt.Println(time.Now(), ":recv false. continue")
continue
}
// 如果为true就停止吧
fmt.Println(time.Now(), ":recv true. return")
return
//过期了
case <-timer.C:
fmt.Println(time.Now(), ":timer expired")
continue
}
}
}()
// 阻塞用
var s string
fmt.Scanln(&s)
返回结果
2021-08-19 13:27:56.968569 +0800 CST m=+1.002051349 :recv false. continue
2021-08-19 13:27:57.968956 +0800 CST m=+2.002434837 :recv false. continue
2021-08-19 13:27:58.971097 +0800 CST m=+3.004571872 :recv false. continue
2021-08-19 13:27:59.97512 +0800 CST m=+4.008591506 :recv false. continue
2021-08-19 13:28:00.975604 +0800 CST m=+5.009071140 :recv false. continue
2021-08-19 13:28:01.97907 +0800 CST m=+6.012533936 :recv true. return
错误情况
c := make(chan bool)
go func() {
// 生产
for i := 0; i < 5; i++ {
time.Sleep(time.Second * 6)
c <- false
}
time.Sleep(time.Second * 1)
c <- true
}()
go func() {
// 消费
timer := time.NewTimer(time.Second * 5)
for {
// 如果过期了将,chan消费完
if !timer.Stop() {
<-timer.C
}
timer.Reset(time.Second * 5)
select {
// 没有过期,判断chan里的数据
case b := <-c:
if b == false {
fmt.Println(time.Now(), ":recv false. continue")
continue
}
// 如果为true就停止吧
fmt.Println(time.Now(), ":recv true. return")
return
//过期了
case <-timer.C:
fmt.Println(time.Now(), ":timer expired")
continue
}
}
}()
// 阻塞用
var s string
fmt.Scanln(&s)
返回结果(直接阻塞在这里)
2021-08-19 13:30:04.949688 +0800 CST m=+5.006256731 :timer expired
问题原因: 因为生产等了6秒,消费的timer已经过期了,然后在进入到!timer.Stop()(已经过期,在执行stop就为false),在timer.C(chan) 执行 <-,就抛错(阻塞住了)
解决方法
c := make(chan bool)
go func() {
// 生产
for i := 0; i < 5; i++ {
time.Sleep(time.Second * 6)
c <- false
}
time.Sleep(time.Second * 1)
c <- true
}()
go func() {
// 消费
timer := time.NewTimer(time.Second * 5)
for {
// 如果stop失败进去
if !timer.Stop() {
// <-timer.C失败直接走默认---往下接着走
select {
case <-timer.C:
default:
}
}
timer.Reset(time.Second * 5)
select {
case b := <-c:
// false打印接着跑
if b == false {
fmt.Println(time.Now(), ":recv false. continue")
continue
}
// 返回true就停止
fmt.Println(time.Now(), ":recv true. return")
return
case <-timer.C:
fmt.Println(time.Now(), ":timer expired")
continue
}
}
}()
// 阻塞用
var s string
fmt.Scanln(&s)
源码
//Tick是NewTicker的一个方便使用的包,仅提供对ticking chan 的访问.
// 返回tick的chan,如果d <= 0 则返回 nil
func Tick(d Duration) <-chan Time {
if d <= 0 {
return nil
}
return NewTicker(d).C
}
注意: time.Tick
底层的Ticker
不能被垃圾收集器恢复。
源码
// NewTicker返回一个新的Ticker,其中包含chan,chan将以d 参数指定的时间段发送时间。
func NewTicker(d Duration) *Ticker {
if d <= 0 {
panic(errors.New("non-positive interval for NewTicker"))
}
// 为chan提供一个一个元素的时间类型缓冲。
c := make(chan Time, 1)
t := &Ticker{
C: c,
r: runtimeTimer{
when: when(d),
period: int64(d),
f: sendTime,
arg: c,
},
}
startTimer(&t.r)
return t
}
// 关闭 Ticker, 不会关闭chan,以防止因并发读取到错误的 tick 的chan信息
func (t *Ticker) Stop() {
stopTimer(&t.r)
}
ticker := time.NewTicker(time.Second * 1)
go func() {
for t := range ticker.C {
fmt.Println("i am come at ", t)
}
}()
time.Sleep(time.Second * 5)
ticker.Stop()
看了创建方法的时候应该可以知道,Ticker
中的runtimeTimer
字段的 period
字段会赋值为 NewTicker(d Duration)
中的d
,表示每间隔d
纳秒,Timer定时器就周期性地触发时间事件,timer触发一次
Go 内存泄露之痛,这篇把 Go timer.After 问题根因讲透了!https://mp.weixin.qq.com/s/KSBdPkkvonSES9Z9iggElgGo语言计时器的使用详解https://mp.weixin.qq.com/s/QahprdKrlcaatG8poWsNrA难以驾驭的 Go timer,一文带你参透计时器的奥秘https://mp.weixin.qq.com/s/gxX-q2EvgWZEWe-deRITSw