golang的定时器虽然使用起来很简单,但是依然又一些细节需要注意的。

首先来看一个例子

func s1() {
	var count int
	for {
		select {
		case <-time.Tick(time.Second * 1):
			fmt.Println("case1")
			count++
			fmt.Println("count--->" , count)
		case <-time.Tick(time.Second * 2) :
			fmt.Println("case2")
			count++
			fmt.Println("count--->" , count)
		}
	}
}

执行结果

case1
count---> 1
case1
count---> 2
case1
count---> 3
case1
count---> 4
time.Tick
func Tick(d Duration) <-chan Time {
	if d <= 0 {
		return nil
	}
	return NewTicker(d).C
}

它每次都会创建一个新的定时器,随着 for 循环进行, select 始终监听两个新创建的定时器,老的定时器被抛弃掉了,也就不会去读取老定时器中的通道。

select 可以同时监听多个通道,谁先到达就先读取谁,如果同时有多个通道有消息到达,那么会随机读取一个通道,其他的通道由于没有被读取,所以数据不会丢失,需要循环调用 select 来读取剩下的通道。

触发定时器

定时器拥有一个长度为 1 的缓冲通道,这保证了系统可以去触发这个定时器,而不用关心是否有 goroutine 在读取这个通道,触发定时器:

func sendTime(c interface{}, seq uintptr) {
	// Non-blocking send of time on c.
	// Used in NewTimer, it cannot block anyway (buffer).
	// Used in NewTicker, dropping sends on the floor is
	// the desired behavior when the reader gets behind,
	// because the sends are periodic.
	select {
	case c.(chan Time) <- Now():
	default:
	}
}

出于安全考虑,go的定时器触发被设置为非阻塞的,这也好理解,如果定时器被触发后,而应用程序始终不来读取通道信息,那么再次触发定时器岂不是要阻塞在那里,这将极大消耗系统资源,因此选择直接跳过。

回收定时器

那么定时器何时被 gc 呢?

Timer

由于 Timer 是一次性的,一旦被触发了,就会被交给 gc ,但是它的通道要等到被读取后才会被关闭。

Stop
Ticker
StopStop Ticker

可以修改为:

func s2() {
	t1 := time.NewTicker(time.Second * 1)
	t2 := time.NewTicker(time.Second * 2)
	defer t1.Stop()
	defer t2.Stop()

	var count int
	for {
		select {
		case <-t1.C:
			fmt.Println("case1")
			count++
			fmt.Println("count--->" , count)
		case <-t2.C :
			fmt.Println("case2")
			count++
			fmt.Println("count--->" , count)
		}
	}
}

我们经常能看到这样的代码,由于使用的是 Timer ,其实问题不大。

func s3() {
	ch := make(chan struct{}, 1)
	for {
		select {
		case <-ch :
			//
		case <-time.After(time.Second * 1) :
			//
		}
	}
}