golang之定时器问题

3,395 阅读3分钟

case1: ticker的值拷贝问题

func tickerTest1() {
    ticker := *time.NewTicker(time.Second)
    count := 0
    go func() {
        time.Sleep(3*time.Second)
        ticker.Stop()
    }()
    for range ticker.C {
        count ++
        fmt.Println("tickerTest1:", count)
    }
}

func tickerTest2() {
    ticker := time.NewTicker(time.Second)
    count := 0
    go func() {
        time.Sleep(3*time.Second)
        ticker.Stop()
    }()
    for range ticker.C { 
        count ++
        fmt.Println("tickerTest2:", count)
    }
}

func main() {
    go tickerTest1()
    tickerTest2()
}

查看源码结构体定义:

type Ticker struct {
	C <-chan Time 
	r runtimeTimer
}

type runtimeTimer struct {
	tb uintptr
	i  int

	when   int64
	period int64
	f      func(interface{}, uintptr) 
	arg    interface{}
	seq    uintptr
}


func NewTicker(d Duration) *Ticker {
	if d <= 0 {
		panic(errors.New("non-positive interval for NewTicker"))
	}
	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 := *time.NewTicker(time.Second)此句表示:tmp=time.NewTicker(time.Second);ticker:=*tmp;,而ticker.C是一个指针形式,故tmp.Cticker.C的值是一样的,故for range ticker.C是能够接受到值,即本质是tmp.C发送的值,因此可以持续输出;而tmp.rticker.r的地址是不一样的。而在在runtime包中,发现stop的时候有这么一个判断:

if i < 0 || i > last || tb.t[i] != t { //tb.t[i]就是ticker.r的地址,t就是tmp.r的地址
    return false
}

关键在于tb.t[i] != t,则这个条件肯定为真,即不从最小堆四叉树中移除,相当于没有 stop。所以计时器没有停止。

case2: ticker.Stop()并不会关闭Channel

func UseTickerWrong() *time.Ticker {
	ticker := time.NewTicker(5 * time.Second)
	go func(ticker *time.Ticker) {
		for range ticker.C {
			fmt.Println("Ticker1....")
		}
		
		fmt.Println("Ticker1 Stop")
	}(ticker)
	
	return ticker
}

func main() {
	ticker1 := UseTickerWrong()
	time.Sleep(20 * time.Second)
	ticker1.Stop()
}

// 输出结果为:
Ticker1....
Ticker1....
Ticker1....
Ticker1....

并没有最后的Ticker1 Stop,查看Ticker的Stop方法的说明会发现:Stop prevents the Timer from firing. It returns true if the call stops the timer, false if the timer has already expired or been stopped. Stop does not close the channel, to prevent a read from the channel succeeding incorrectly.

\color{red}{为什么不去关闭channel}

首先,创建Ticker的协程并不负责计时,只负责从Ticker的管道中获取事件;其次,系统协程只负责定时器计时,向管道中发送事件,并不关心上层协程如何处理事件。即我们在创建timer的时候会构建runtimeTimer对象,里面有sendTime回调方法及初始化的channeltimerprocgolang runtime的定时扫描器,当发现有任务到期后,进行相应的方法回调。但如果我们在stop里把channel给关闭了,那么timerproc有可能就panic了。

case3: 在定时器触发之前,gc不会回收 Timer

go func() {
	for {
		timerC := time.After(2 * time.Second)
		select {
		case num := <-ch:
			fmt.Println("get num is ", num)
		case <-timerC:
			fmt.Println("time's up !!!")
		}
	}
}()

每循环一次,timerC都是重新创建的,即timerC只对一个select有效。下次处理时重新计算超时时间,每个操作的处理都是独立的。 若ch触发的频道远远高于timerC的触发频道,将会导致timerC不停地被创建。但由于在定时器触发之前,gc是不会对timerC进行回收。因此造成了大量的内存浪费(由于timerC还在四叉树里被引用着,故不能进行gc回收)。可以通过如下方式解决问题:

go func() {
	idleDuration := 5 * time.Second
	idleDelay:=time.NewTimer(idleDuration)
	defer  idleDelay.Stop()
	for {
		idleDelay.Reset(idleDuration) //重置定时器
		select {
		case num := <-ch:
			fmt.Println("get num is ", num)
		case <-idleDelay.C:
			fmt.Println("time's up !!!")
		}
	}
}()

case4:创建的周期性定时器,若不进行主动stop,将会导致内存泄漏

time.NewTicker(duration)创建的周期性定时器,若不进行主动stop,将会导致内存泄漏。原因在于:在源码里,周期性定时器触发以后,又会加入到最小堆四叉树里,若没有外界的干扰,此ticker始终会在最小堆四叉树里,即此ticker始终被引用着,即gc不可能回收此ticker

func  run() {
	timeout := time.NewTicker(p.Timeout)
	interval := time.NewTicker(p.Interval)
	for {
		select {
		case <-p.done:       
			wg.Wait()
			return
		case <-timeout.C:    
			close(p.done)
			wg.Wait()
			return
		case <-interval.C:
			if p.Count > 0 && p.PacketsSent >= p.Count {
				continue
			}
			err = p.sendICMP(conn)
			if err != nil {
				fmt.Println("FATAL: ", err.Error())
			}
		case r := <-recv:
			err := p.processPacket(r)
			if err != nil {
				fmt.Println("FATAL: ", err.Error())
			}
		}
		if p.Count > 0 && p.PacketsRecv >= p.Count {  
			close(p.done)
			wg.Wait()
			return
		}
	}
}

因此创建一个ticker,应该紧跟着使用defer ticker.Stop()语句,当函数退出时自动从最小堆四叉树中移除。修复后的代码为:

func  run() {
	timeout := time.NewTicker(p.Timeout)
	defer timeout.Stop()  
	interval := time.NewTicker(p.Interval)
	defer interval.Stop() 
	for {
		select {
		case <-p.done:       
			wg.Wait()
			return
		case <-timeout.C:   
			close(p.done)
			wg.Wait()
			return
		case <-interval.C:
			if p.Count > 0 && p.PacketsSent >= p.Count {
				continue
			}
			err = p.sendICMP(conn)
			if err != nil {
				fmt.Println("FATAL: ", err.Error())
			}
		case r := <-recv:
			err := p.processPacket(r)
			if err != nil {
				fmt.Println("FATAL: ", err.Error())
			}
		}
		if p.Count > 0 && p.PacketsRecv >= p.Count { 
			close(p.done)
			wg.Wait()
			return
		}
	}
}