赞
踩
上篇文章写了关于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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。