定时器:timer

time包中有两个函数可以帮我们初始化 time.Timer (类型)

在高性能场景下,不应该使用time.After,而应该使用New.Timer并在不再使用该Timer后(无论该Timer是否被触发)调用Timer的Stop方法来及时释放资源。不然内存资源可能被延时释放。

Newtimer

1
time.Newtimer()

初始化一个到期时间据此时的间隔3小时30分的定时器

1
t := time.Newtimer(3*time.Hour + 30*time.Minute)

其中 t 是 *time.NewTimer 类型的,这个指针类型的方法集合包含两个方法:

  • Reset() 用于重置定时器,该方法返回一个bool类型的值
  • Stop() 用来停止定时器,该方法返回一个bool类型的值,如果返回false,说明该定时器在之前已经到期或者已经被停止了,反之返回true。

通过定时器的字段 C,我们可以及时得知定时器到期的这个事件来临,C 是一个 chan time.Time类型的缓冲通道,一旦触及到期时间,定时器就会向自己的 C 字段发送一个 time.Time类型的元素值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import (
    "time"
    "fmt"
)

func main() {
    t := time.NewTimer(3 * time.Second)

    fmt.Printf("Time now = %v\n", time.Now())
    fmt.Printf("Expiration time = %v\n", <- t.C)
}
/** output:
Time now = 2018-11-27 09:22:09.92001771 +0800 CST m=+0.000332590
Expiration time = 2018-11-27 09:22:12.921125657 +0800 CST m=+3.001270537
*/

Timer.Reset

1
func (t *Timer) Reset(d Duration) bool

Reset使t重新开始计时,(本方法返回后再)等待时间段d过去后到期。

函数的返回值是bool类型,如果调用时t未到期会返回true;如果t已经到期或者被停止了会返回false。

我们所期望的定时器设置失败与否,通常只和通道有关:设置定时器前,定时器的通道Timer.C中是否已经有数据。

  • 如果有,我们设置的定时器失败了,我们可能读到不正确的超时事件。
  • 如果没有,我们设置的定时器成功了,我们在设定的时间得到超时事件。

因为定时器的缓存通道大小只为1,无法多存放超时事件,而且添加到通道的方法是非阻塞IO,如果通道已经有值,就会直接放弃.

定时器创建后是单独运行的,超时后会向通道写入数据,你从通道中把数据读走。当前一次的超时数据没有被读取,而设置了新的定时器,然后去通道读数据,结果读到的是上次超时的超时事件,看似成功,实则失败.

Timer.Stop

调用Stop方法可阻止Timer被触发。如果Stop调用停止了timer,那么返回true,如果timer已经被触发过或者已经被停止了,那么返回false。

Stop方法在timer未被触发或已被触发或已被关闭的情况下都可以被调用。在高性能场景下,我们应该统一在select结束后,即timer不再使用后调用Stop方法。如果timer不做超时取消释放资源,则可能因为依赖方响应缓慢而导致本地资源堆积,例如 fd,连接数,内存占用等等。从而导致服务宕机。

time.Afer

time.Afer

time.After 一般用来控制某些耗时较长的行为,在超时后不再等待,以使程序行为可预期,等价于time.NewTimer(d).C

After函数底层所使用的Timer直到timer被触发才会被golang的垃圾回收器处理。但何时进一步释放给操作系统由垃圾回收器决定。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
    "time"
    "fmt"
)

func main() {
    ch1 := make(chan int, 1)
    ch2 := make(chan int, 1)
    // ch1 <- 1
    select {
    case e1 := <-ch1:
        //如果ch1通道成功读取数据,则执行该case处理语句
        fmt.Printf("1th case is selected. e1=%v", e1)
    case e2 := <-ch2:
        //如果ch2通道成功读取数据,则执行该case处理语句
        fmt.Printf("2th case is selected. e2=%v", e2)
    case <-time.After(2 * time.Second):
        fmt.Println("Timed out")
    }
}
// output: Timed out

time.Sleep() 函数,表示休眠多少时间,休眠时出于阻塞状态,后续程序无法执行。

time.Aferfunc

自定义定时器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
    "fmt"
    "time"
)
func main(){
    var t *time.Timer

    f := func(){
        fmt.Printf("Expiration time : %v.\n", time.Now())
        fmt.Printf("C`s len: %d\n", len(t.C))
    }

    t = time.AfterFunc(1*time.Second, f)
    //让当前Goroutine 睡眠2s,确保大于内容的完整
    //这样做原因是,time.AfterFunc的调用不会被阻塞。它会以一部的方式在到期事件来临执行我们自定义函数f。
    time.Sleep(2 * time.Second)
}
/** output:
Expiration time : 2018-11-27 10:09:22.084781987 +0800 CST m=+1.003211639.
C`s len: 0
 */

第二行打印内容说明:定时器字段 C 并没有缓冲任何元素值,这也说明了,在给定了自定义函数后,默认的处理方法(向C发送代表绝对到期时间的元质素)就不会被执行了。

断续器:ticker

ticker就是周期性的传达到期时间的装置,这种装置的行为方式与仅有秒针的钟表有些类似

time包中有两个函数可以帮我们初始化 time.Ticker (类型):

NewTicker

1
var ticker *time.Ticker = time.NewTicker(time.Second)

Ticker.Stop

使用time.NewTicker时,在Ticker对象不再使用后(无论该Ticker是否被触发过),一定要调用Stop方法,否则会造成内存和cpu泄漏。

除非程序终止前定时器一直需要触发,否则,不需要时应该调用 Ticker.Stop 来释放相关资源。

1
2
3
ticker := time.NewTicker(1 * time.Second)
<-ticker.C    // receive from the ticker's channel
ticker.Stop() // cause the ticker's goroutine to terminate

time.Tick

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import (
    "fmt"
    "time"
)

func main() {
    for t := range time.Tick(time.Second * 2) {
        fmt.Println(t, "hello world")
    }
}

Tick函数挺方便,但是只有当程序整个生命周期都需要这个时间时我们使用它才比较合适。否则的话,我们应该使用NewTicker.

time.After的正确用法

问题

time.After是一次性触发的,触发后 timer 本身会从时间堆中删除。所以一般情况下直接用 <-time.After 是没有问题的,不过在 for 循环的时候要注意:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import "time"

func main() {
    var ch = make(chan int)
    go func() {
        for {
            ch <- 1
        }
    }()

    for {
        select {
        case <-time.After(time.Second):
            println("time out, and end")
        case <-ch:
        }
    }
}

上面的代码,<-ch 这个 case 每次执行的时间都很短,但每次进入 select,time.After 都会分配一个新的 timer。因此会在短时间内创建大量的无用 timer,虽然没用的 timer 在触发后会消失,但这种写法会造成无意义的 cpu 资源浪费。

解决

正确的写法应该对 timer 进行重用,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "time"

func main() {
    var ch = make(chan int)
    go func() {
        for {
            ch <- 1
        }
    }()

    timer := time.NewTimer(time.Second)
    for {
        timer.Reset(time.Second)
        select {
        case <-timer.C:
            println("time out, and end")
        case <-ch:
        }
    }
}

如果之前的 timer 没用了,可以手动 Stop 以使该 timer 从时间堆中移除。

1
2
timer := time.NewTimer(time.Second)
timer.Stop()

Stop与Reset的正确用法

问题

按照 Timer.Stop 文档 的说法,每次调用 Stop 后需要判断返回值,如果返回 false(表示 Stop 失败,Timer 已经在 Stop 前到期)则需要排掉(drain)channel 中的事件:

1
2
3
if !t.Stop() {
	<-t.C
}

但是如果之前程序已经从 channel 中接收过事件,那么上述 <-t.C 就会发生阻塞。可能的解决办法是借助 select 进行 非阻塞 排放(draining):

1
2
3
4
5
6
if !t.Stop() {
	select {
	case <-t.C: // try to drain the channel
	default:
	}
}

但是因为 channel 的发送和接收发生在不同的 goroutine,所以 存在竞争条件(race condition),最终可能导致 channel 中的事件未被排掉。

以下就是一种有问题的场景,按时间先后顺序发生:

goroutine A:Go 运行时判断 Timer 已经到期,于是从最小堆中删除该 Timer goroutine B:应用程序执行 Timer.Stop,发现 Timer 已经到期,进而返回 false goroutine B:应用程序继续执行 select…case <-t.C,因为 channel 中并没有事件,所以会立即返回 goroutine A:Go 运行时将到期事件发送到该 Timer 的 channel 中

按照 Timer.Reset 文档 的说法,要正确地 Reset Timer,首先需要正确地 Stop Timer。因此 Reset 的问题跟 Stop 基本相同。

竞争条件

如果你看过timerproc的代码,你会发现其中的这样一段代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// $GOROOT/src/runtime/time.go
            f := t.f
            arg := t.arg
            seq := t.seq
            unlock(&timers.lock)
            if raceenabled {
                raceacquire(unsafe.Pointer(t))
            }
            f(arg, seq)
            lock(&timers.lock)

我们看到在timerproc执行f(arg, seq)这个函数前,timerproc unlock了timers.lock,也就是说f的执行并没有在锁内。

前面说过,f的执行是什么?

  • 对于AfterFunc来说,就是启动一个goroutine,并在这个新goroutine中执行用户传入的函数;
  • 对于After和NewTimer这种创建姿势创建的timer而言,f的执行就是sendTime的执行,也就是向t.C中send 当前时间。

注意:这时候timer expire过程中sendTime的执行与“drain channel”是分别在两个goroutine中执行的,谁先谁后,完全依靠runtime调度。

  • 如果sendTime的执行发生在drain channel执行前,那么就是example4.go中的执行结果:Stop返回false(因为timer已经expire了),显式drain channel会将数据读出,后续Reset后,timer正常执行;
  • 如果sendTime的执行发生在drain channel执行后,那么问题就来了,虽然Stop返回false(因为timer已经expire),但drain channel并没有读出任何数据。之后,sendTime将数据发到channel中。timer Reset后的Timer中的Channel实际上已经有了数据,于是当进入下面的select执行体时,”case <-timer.C:”瞬间返回,触发了timer事件,没有启动超时等待的作用。

解决

参考 Russ Cox 的回复,目前 Timer 唯一合理的使用方式是:

  • 程序始终在同一个 goroutine 中进行 Timer 的 Stop、Reset 和 receive/drain channel 操作
  • 程序需要维护一个状态变量,用于记录它是否已经从 channel 中接收过事件,进而作为 Stop 中 draining 操作的判断依据

如果每次使用 Timer 都要按照上述方式来处理,无疑是一件很费神的事。Go 库 goodtimer 解决标准 Timer 的问题。

源代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package goodtimer

import (
	"time"
)

// GoodTimer wraps the standard time.Timer to provide more user-friendly interfaces.
//
// **NOTE**: All the the functions of GoodTimer *should* be used in the same goroutine.
type GoodTimer struct {
	t    *time.Timer // The actual timer
	read bool        // Whether t.C has already been read from
}

// NewGoodTimer creates an instance of GoodTimer.
func NewGoodTimer(t *time.Timer) *GoodTimer {
	return &GoodTimer{t: t}
}

// ReadC waits until it can read from the wrapped timer's channel C.
// It returns the time value received from the channel C, a zero time value if the channel C has already been read from.
func (gt *GoodTimer) ReadC() time.Time {
	if gt.read {
		return time.Time{}
	}
	tv := <-gt.t.C
	gt.read = true
	return tv
}

// TryReadC waits for at most the duration d, in order to read from the wrapped timer's channel C.
// It returns the time value received from the channel C, a zero time value if the channel C has already been read from or if the timeout is reached.
func (gt *GoodTimer) TryReadC(timeout time.Duration) time.Time {
	if gt.read {
		return time.Time{}
	}
	select {
	case tv := <-gt.t.C:
		gt.read = true
		return tv
	case <-time.After(timeout):
		return time.Time{}
	}
}

// Reset changes the timer to expire after duration d.
func (gt *GoodTimer) Reset(d time.Duration) {
	gt.Stop()
	gt.t.Reset(d)
	gt.read = false
}

// 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.
func (gt *GoodTimer) Stop() bool {
	stopped := gt.t.Stop()
	if !stopped && !gt.read {
		// Drain the gt.t.C if it has not been read from already
		<-gt.t.C
	}
	return stopped
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package example

import (
	"fmt"
	"time"

	"github.com/goodtimer"
)

func Example_blockingRead() {
	t := time.NewTimer(2 * time.Second)
	gt := goodtimer.NewGoodTimer(t)

	// Read from the wrapped timer's channel C.
	if tv := gt.ReadC(); !tv.IsZero() {
		fmt.Println("The timer fires")
	}

	// Output:
	// The timer fires
}

func Example_nonBlockingRead() {
	t := time.NewTimer(2 * time.Second)
	gt := goodtimer.NewGoodTimer(t)

	// Read from the wrapped timer's channel C, in a non-blocking way.
	if tv := gt.TryReadC(1 * time.Second); tv.IsZero() {
		fmt.Println("Timed out before the timer firing")
	}

	// Output:
	// Timed out before the timer firing
}

func Example_stop() {
	t := time.NewTimer(2 * time.Second)
	gt := goodtimer.NewGoodTimer(t)

	// Any operations in the current goroutine.

	if gt.Stop() {
		fmt.Println("The timer is stopped before firing")
	}

	// Output:
	// The timer is stopped before firing
}

func Example_reset() {
	t := time.NewTimer(2 * time.Second)
	gt := goodtimer.NewGoodTimer(t)

	// Any operations in the current goroutine.

	gt.Reset(2 * time.Second)

	// Now you can use the timer gt again.
}

Stop的正确用法

问题

比如,在时间轮里实例化了一个timer定时器对象,然后我在另一个协程里调用timer.Stop()来关闭该定时器,但 <- timer.C不会被通知到,不会被通知就一直被阻塞。

为什么被阻塞,因为我的时间轮代码里只是对定时任务的删除,而没有去close channel,timer.C自然就阻塞。我这边肯定不能直接粗暴的去close channel,因为这样有概率触发panic send on closed channel的问题。

让我们先来看下golang标准库里timer、ticker关于stop方法的实现。src/time/sleep.go里的timer stop调用的是stopTimer方法,最终的stopTimer方法在runtime/time.go里,该方法是使用go:linkname来做的映射。最后调用的是deltimer,该方法的逻辑很简单,就是在heap里删除对应的定时任务,这就完事了….

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func (t *Timer) Stop() bool {
    if t.r.f == nil {
        panic("time: Stop called on uninitialized Timer")
    }
    return stopTimer(&amp;t.r)
}

//go:linkname stopTimer time.stopTimer
func stopTimer(t *timer) bool {
    return deltimer(t)
}

// Delete timer t from the heap.
// Do not need to update the timerproc: if it wakes up early, no big deal.
func deltimer(t *timer) bool {
    if t.tb == nil {
        return false
    }

    tb := t.tb

    lock(&amp;tb.lock)
    removed, ok := tb.deltimerLocked(t)
    unlock(&amp;tb.lock)
    if !ok {
        badTimer()
    }
    return removed
}

搜寻了半天,在src/time/sleep.go, src/runtime/time.go里没找到关闭channel的逻辑。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// xiaorui.cc

// The Timer type represents a single event.
// When the Timer expires, the current time will be sent on C,
// unless the Timer was created by AfterFunc.
// A Timer must be created with NewTimer or AfterFunc.
type Timer struct {
    C <-chan Time
    r runtimeTimer
}

最后不经意间,在Stop()里找到有关close channel的问题说明。

为什么不去close channel?

我们在创建timer的时候会构建runtimeTimer对象,里面有sendTime回调方法及初始化的channel。timerproc是golang runtime的定时扫描器,当发现有任务到期后,进行相应的方法回调。但如果我们在stop里把channel给关闭了,那么timerproc有可能就panic了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// timer定时器的定义
func NewTimer(d Duration) *Timer {
    c := make(chan Time, 1) // buf为1, 主要为了优化timerproc的回调性能
    t := &Timer{
        C: c,
        r: runtimeTimer{
            when: when(d),  // 时间
            f:    sendTime, // 回调方法
            arg:  c,        // 参数
        },
    }
    startTimer(&t.r)
    return t
}

// 回调方法, default用来负责send.
func sendTime(c interface{}, seq uintptr) {
    select {
    case c.(chan Time) <- Now():
    default:
    }
}

// runtime/time.go
func timerproc(tb *timersBucket) {
    ...
    f := t.f
    arg := t.arg
    seq := t.seq
    unlock(&tb.lock)
    if !ok {
        badTimer()
    }
    ...
    f(arg, seq)  // 有可能会触发 panic send onclosed channel ...
    lock(&tb.lock)
    ...
}

解决

既然他不去close channel,那么我们可以通过context或者创建一个stop channel来做事件的通知。

context

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctimer()
    fmt.Printf("exit")
    time.Sleep(10 * time.Second)
}

func ctimer() {
    timer := time.NewTimer(5 * time.Second)
    ctx, cancel := context.WithCancel(context.Background())
    go func() {
        time.Sleep(2 * time.Second)
        timer.Stop()
        cancel()
        fmt.Println("stop")
    }()

    for {
        select {
        case <-timer.C:
            fmt.Println("ticker.C call")
            return

        case <-ctx.Done():
            return
        }
    }
}

stop channel

下面代码通过startTicker创建一个ticker, 当想要关闭这个ticker并同时退出对应goroutine中的for时,可以直接close(),发送done信号直接返回退出startTicker即可.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main
import (
	"fmt"
	"time"
)

func main() {
	Demo()
}

func Demo() {
	done := startTicker(PrintInfo)
	time.Sleep(time.Duration(12) * time.Second)
	close(done)
	time.Sleep(time.Duration(1) * time.Minute)
	fmt.Println("main finished")
}

func startTicker(f func()) chan struct{} {
	done := make(chan struct{}, 1)
	go func() {
		ticker := time.NewTicker(5 * time.Second)
		//	ticker := time.NewTicker(5 * time.Minute)
		defer ticker.Stop()

		for {
			select {
			case <-ticker.C:
				f()
			case <-done:
				return
			}
		}
	}()
	return done
}

func PrintInfo() {
	fmt.Println("hello")
}

参考: https://lg1024.com/post/go_timer_ticker.html https://segmentfault.com/a/1190000017013443 http://russellluo.com/2018/09/the-correct-way-to-use-timer-in-golang.html https://tonybai.com/2016/12/21/how-to-use-timer-reset-in-golang-correctly/ http://xiaorui.cc/2019/09/09/%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90go-time-timer%E5%92%8Cticker%E7%9A%84stop%E9%97%AE%E9%A2%98/ http://researchlab.github.io/2016/10/16/close-ticker-correctly/