Golang并发编程-定时器Timer & Ticker详解

目录

  • 1 Golang中的定时器
  • 2 Timer
    • 2.1 关于Timer
    • 2.2 Timer的结构体方法
      • 2.2.1 Stop
      • 2.2.2 Reset
    • 2.3 创建Timer
      • 2.3.1 NewTimer
      • 2.3.2 AfterFunc
      • 2.3.3 After
    • 2.4 使用示例
      • 2.4.1 使用Timer实现延时执行
      • 2.4.2使用Timer实现超时控制
  • 3 Ticker
    • 3.1 关于Ticker
    • 3.2 Tiker的结构体方法
      • 3.2.1 Stop
      • 3.2.2 Reset
    • 3.3 创建Tiker
    • 3.4 使用示例
      • 3.4.1 循环执行

1 Golang中的定时器

Golang中有两种类型的定时器:

  • Timer

Timer 用于延迟执行某个任务

  • Ticker

Ticker 用于间隔执行某个任务。

不罗嗦了,我们直接开始看这两种定时器的具体定义和使用!

2 Timer

2.1 关于Timer

Timer结构体源码:

type Timer struct {
	C <-chan Time
	r runtimeTimer
}
  • Timer代表了单个时间:当到达Timer指定的时间后,当前时间会被发送到C <-chan Time通道上,除非Timer是由AfterFunc方法创建的。(后文会介绍)
  • Timer必须由NewTimerAfterFunc方法创建。(后文会介绍)

2.2 Timer的结构体方法

2.2.1 Stop

源码:

func (t *Timer) Stop() bool {
	if t.r.f == nil {
		panic("time: Stop called on uninitialized Timer")
	}
	return stopTimer(&t.r)
}

Stop方法用于停止计时器计时,从而防止定时器触发。

  • 如果调用Stop方法成功停止了计时器,方法返回true。如果定时器已经到期(触发过了)或者已经被Stop过了,返回false。
  • Stop方法不会关闭定时器的通道,以防止从通道错误地读取成功。
  • 为了确保调用Stop后定时器的通道是空的,可以检查Stop方法的返回值并排空通道:
    if !t.Stop() { // 如果Stop失败
    	<-t.C // 排空通道
    }
    
  • 对于使用AfterFunc(d, f)创建的计时器,如果 t.Stop 返回 false,则计时器已经到期,并且函数 f 已在其自己的 goroutine 中启动; Stop 不会等待 f 完成才返回。如果调用者需要知道 f 是否完成,它必须显式地与 f 协调。

2.2.2 Reset

源码

func (t *Timer) Reset(d Duration) bool {
	if t.r.f == nil {
		panic("time: Reset called on uninitialized Timer")
	}
	w := when(d)
	return resetTimer(&t.r, w)
}

Reset方法将定时器的过期时间设置为新的,即当前时间过d Duration时间后.

  • 如果定时器已激活(had been active),将返回true。如果定时器已经到期或者已经被停止,方法返回false。注意这里返回的true和false并不代表是否Reset成功!!!看示例:
    func main() {
    	timer := time.NewTimer(time.Second * 5) // 计时5秒
    
    	time.Sleep(time.Second)
    	log.Println(timer.Stop()) // 一秒后停止计时 :true
    
    	time.Sleep(time.Second)
    	log.Println(timer.Reset(time.Second * 5)) // 再过一秒后Reset成5秒:false
    
    	log.Println(<-timer.C) // 输出的是重新Reset,5秒后的时间。
    }
    
    可以看到,Reset时由于计时器已经被Stop,返回了false。实际上还是设置了新的过期时间为5秒。

    请注意,不可能正确使用 Reset 的返回值,因为在耗尽通道和新计时器到期之间存在竞争条件。如上所述,应始终在已停止或过期的通道上调用重置。返回值的存在是为了保持与现有程序的兼容性。

  • 对于NewTimer方法创建的Timer,调用Reset方法时,应该确保Timer是一个通道中没有值的已过期或已停止的计时器。如果通道不为空,接收方可能会收到两个结果:一个原本的,一个新的。
  • 如果程序已经从 t.C 接收到一个值,则知道计时器已到期并且通道已耗尽,因此可以直接使用 t.Reset。但是,如果程序尚未从 t.C 接收到值,则必须停止计时器,并且如果 Stop 报告计时器在停止之前已过期,则通道需要显式耗尽:
    if !t.Stop() {
    	<-t.C
    }
    t.Reset(d)
    
  • 对于使用 AfterFunc(d, f) 创建的计时器,Reset 要么重新安排 f 运行的时间(在这种情况下 Reset 返回 true),要么安排 f 再次运行(在这种情况下返回 false)。当 Reset 返回 false 时,Reset 既不会等待前一个 f 完成才返回,也不保证后续运行 f 的 goroutine 不会与前一个 goroutine 并发运行。如果调用者需要知道 f 的先前执行是否完成,它必须显式地与 f 协调。

2.3 创建Timer

前面也说了, Timer必须由NewTimerAfterFunc方法创建。现在就来看看怎么使用这两个方法。

2.3.1 NewTimer

方法源码:

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
}
  • NewTimer返回一个Timer,在经过指定的时间间隔d Duration后,Timer会将当前时间发送到其C通道上。

2.3.2 AfterFunc

方法源码:

func AfterFunc(d Duration, f func()) *Timer {
	t := &Timer{
		r: runtimeTimer{
			when: when(d),
			f:    goFunc,
			arg:  f,
		},
	}
	startTimer(&t.r)
	return t
}
  • AfterFunc方法在指定的时间间隔d Duration后,在自己的goroutine中自动调用指定的方法f func()
  • AfterFunc方法返回的Timer可以用于取消调用(使用Stop方法)。
  • AfterFunc方法返回的Timer的C通道是nil的。

2.3.3 After

除了NewTimerAfterFunc方法外,After方法可以创建定时器,但是After方法实际上是通过调用NewTimer方法创建的。看源码:

func After(d Duration) <-chan Time {
	return NewTimer(d).C
}

从源码可以看到,After方法调用NewTimer方法创建了一个Timer,但是只返回了Timer的C通道

这意味着在指定时间到达之前,调用者没有无法通过调用Timer.Stop方法来停止计时,垃圾收集器不会回收底层计时器。

所以如果担心效率,请改用 NewTimer,并在不再需要计时器时调用 Timer.Stop

2.4 使用示例

2.4.1 使用Timer实现延时执行

场景:在指定时间间隔后,开始执行指定任务。

func main() {
	// 创建一个5秒的定时器
	timer := time.NewTimer(time.Second * 5)
	
	// 起一个协程,5秒后执行domSomeThing方法
	go func() {
		<-timer.C
		doSomeThing()
	}()
	
	// 主线程继续执行其他的
	// 这里睡眠等待子协程输出
	time.Sleep(time.Second * 6)
}

func doSomeThing() {
	log.Println("DO")
}

上面的示例也可以将NewTimer替换成使用After方法:

func main() {
	// 创建一个5秒的定时器
	c := time.After(time.Second * 5)

	// 起一个协程,5秒后执行domSomeThing方法
	go func() {
		<-c
		doSomeThing()
	}()

	// 主线程继续执行其他的
	// 这里睡眠等待子协程输出
	time.Sleep(time.Second * 6)
}

func doSomeThing() {
	log.Println("DO")
}

当然,这里使用AfterFunc方法好像更加优雅

func main() {
	// AfterFunc方法将在5秒后执行doSomeThing方法
	_ = time.AfterFunc(time.Second*5, doSomeThing)

	// 主线程继续执行其他的
	// 这里睡眠等待子协程输出
	time.Sleep(time.Second * 6)
}

func doSomeThing() {
	log.Println("DO")
}

2.4.2使用Timer实现超时控制

场景:执行一个比较耗时的操作doSomeThing,如果执行耗时超过5秒,就打印超时日志。

func main() {
	// 定时器设置超时时间为4秒
	timer := time.NewTimer(time.Second * 4)

	// resultChan 用以接收返回结果
	resultChan := make(chan int, 1)

	// 起一个协程去执行耗时操作
	go func() {
		resultChan <- doSomeThing() // 执行结果放入结果通道
	}()

	// 监听两个通道
	select {
	case <-timer.C:
		log.Println("doSomeThing timeout.")
	case r := <-resultChan:
		log.Printf("doSomeThing got result:%d\n", r)
	}

	// 这里等2秒,等待子协程输出
	time.Sleep(2 * time.Second)
}

func doSomeThing() int {
	log.Println("doSomeThing start.")
	time.Sleep(5 * time.Second) // 模拟耗时操作
	log.Println("doSomeThing end.")
	return 100
}

输出结果:

2024/07/24 09:49:07 doSomeThing start.
2024/07/24 09:49:11 doSomeThing timeout.
2024/07/24 09:49:12 doSomeThing end.

3 Ticker

3.1 关于Ticker

Ticker源码:

type Ticker struct {
	C <-chan Time // The channel on which the ticks are delivered.
	r runtimeTimer
}
  • Ticker也持有一个C通道,每经过指定的时间间隔,Ticker就会向该通道上发送当前时间(时钟滴答声)。
  • 如果C通道的接收者不能及时取走通道上的数据(即接收者在新的时间到达时,还没有从通道取走上一次的时间),Ticker将调整时间间隔,或者废弃部分时间(时钟滴答声):
    // sendTime does a non-blocking send of the current time on c.
    func sendTime(c any, seq uintptr) {
    	select {
    	case c.(chan Time) <- Now():
    	default:
    	}
    }
    

3.2 Tiker的结构体方法

3.2.1 Stop

func (t *Ticker) Stop() {
	stopTimer(&t.r)
}
  • Stop方法停止Ticker,Stop后,不会再往C通道上发送当前时间(时钟滴答声)。
  • Stop方法不会关闭C通道,以防止接收者读取到错误的应答。

3.2.2 Reset

func (t *Ticker) Reset(d Duration) {
	if d <= 0 {
		panic("non-positive interval for Ticker.Reset")
	}
	if t.r.f == nil {
		panic("time: Reset called on uninitialized Ticker")
	}
	modTimer(&t.r, when(d), int64(d), t.r.f, t.r.arg, t.r.seq)
}

  • Reset方法stop Ticker,并将其时间间隔设置成新的时间间隔。
  • 下一个(时钟滴答声)将在新的时间间隔后发送。
  • 新的时间间隔必须大于0,否则会panic。

3.3 创建Tiker

创建Ticker只有NewTicker一个方法:

func NewTicker(d Duration) *Ticker {
	if d <= 0 {
		panic("non-positive interval for NewTicker")
	}
	// Give the channel a 1-element time buffer.
	// If the client falls behind while reading, we drop ticks
	// on the floor until the client catches up.
	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
}
  • 时间间隔d必须大于0,否则将会panic。

3.4 使用示例

3.4.1 循环执行

场景:每过指定时间间隔,执行一次任务。

func main() {
	// 指定时间间隔为5秒
	ticker := time.NewTicker(time.Second * 5)
	for {
		<-ticker.C
		// 每5秒执行一次
		doSomeThing()
	}
}

func doSomeThing() {
	log.Println("doSomeThing start.")
	time.Sleep(1 * time.Second) // 模拟耗时操作
	log.Println("doSomeThing end.")
}

你可能感兴趣的:(Golang,golang,开发语言,后端)