Golang定时器实现

诺唯 · · 2569 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

这篇文章简单的介绍下golang time 包下定时器的实现,说道定时器,在我们开发过程中很常用,由于使用的场景不同,所以对定时器实际的实现也就不同,go的定时器并没有使用SIGALARM信号实现,而是采取最小堆的方式实现(源码包中使用数组实现的四叉树),使用这种方式定时精度很高,但是有的时候可能我们不需要这么高精度的实现,为了更高效的利用资源,有的时候也会实现一个精度比较低的算法。

跟golang定时器相关的入口主要有以下几种方法:

<-time.Tick(time.Second)
<-time.After(time.Second)
<-time.NewTicker(time.Second).C
<-time.NewTimer(time.Second).C
time.AfterFunc(time.Second, func() { /*do*/ })
time.Sleep(time.Second)

这里我们以其中NewTicker为入口,NewTicker的源码如下:

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(d)返回一个runtimeNano() + int64(d)的未来时(到期时间)
			//runtimeNano运行时当前纳秒时间
			when:   when(d),
			period: int64(d), // 被唤醒的时间
			f:      sendTime, // 时间到期后的回调函数
			arg:    c,        // 时间到期后的断言参数
		},
	}
	// 将新的定时任务添加到时间堆中
	// 编译器会将这个函数翻译为runtime.startTimer(t *runtime.timer)
	// time.runtimeTimer翻译为runtime.timer
	startTimer(&t.r)
	return t

这里有个比较重要的是startTimer(&t.r)它的实现被翻译在runtime包内

func startTimer(t *timer) {
	if raceenabled {
		racerelease(unsafe.Pointer(t))
	}
	addtimer(t)
}

func addtimer(t *timer) {
	lock(&timers.lock)
	addtimerLocked(t)
	unlock(&timers.lock)
}

上面的代码为了看着方便,我将他们都放在一起

下面代码都写出部分注释

// 使用锁将计时器添加到堆中
// 如果是第一次运行此方法则启动timerproc
func addtimerLocked(t *timer) {
	if t.when < 0 {
		t.when = 1<<63 - 1
	}
	// t.i i是定时任务数组中的索引
	// 将新的定时任务追加到定时任务数组队尾
	t.i = len(timers.t)
	timers.t = append(timers.t, t)
	// 使用数组实现的四叉树最小堆根据when(到期时间)进行排序
	siftupTimer(t.i)
	// 如果t.i 索引为0
	if t.i == 0 {
		if timers.sleeping {
			// 如果还在sleep就唤醒
			timers.sleeping = false
			// 这里基于OS的同步,并进行OS系统调用
			// 在timerproc()使goroutine从睡眠状态恢复
			notewakeup(&timers.waitnote)
		}
		if timers.rescheduling {
			timers.rescheduling = false
			// 如果没有定时器,timerproc()与goparkunlock共同sleep
			// goready这里特殊说明下,在线程创建的堆栈,它比goroutine堆栈大。
			// 函数不能增长堆栈,同时不能被调度器抢占
			goready(timers.gp, 0)
		}
	}
	if !timers.created {
		timers.created = true
		go timerproc() //这里只有初始化一次
	}
}

// Timerproc运行时间驱动的事件。
// 它sleep到计时器堆中的下一个。
// 如果addtimer插入一个新的事件,它会提前唤醒timerproc。
func timerproc() {
	timers.gp = getg()
	for {
		lock(&timers.lock)
		timers.sleeping = false
		now := nanotime()
		delta := int64(-1)
		for {
			if len(timers.t) == 0 {
				delta = -1
				break
			}
			t := timers.t[0]
			delta = t.when - now
			if delta > 0 {
				break // 时间未到
			}
			if t.period > 0 {
				// 计算下一次时间
                                // period被唤醒的间隔
				t.when += t.period * (1 + -delta/t.period)
				siftdownTimer(0)
			} else {
				// remove from heap
				last := len(timers.t) - 1
				if last > 0 {
					timers.t[0] = timers.t[last]
					timers.t[0].i = 0
				}
				timers.t[last] = nil
				timers.t = timers.t[:last]
				if last > 0 {
					siftdownTimer(0)
				}
				t.i = -1 // 标记移除
			}
			f := t.f
			arg := t.arg
			seq := t.seq
			unlock(&timers.lock)
			if raceenabled {
				raceacquire(unsafe.Pointer(t))
			}
			f(arg, seq)
			lock(&timers.lock)
		}
		if delta < 0 || faketime > 0 {
			// 没有定时器,把goroutine sleep。
			timers.rescheduling = true
			// 将当前的goroutine放入等待状态并解锁锁。
			// goroutine也可以通过呼叫goready(gp)来重新运行。
			goparkunlock(&timers.lock, "timer goroutine (idle)", traceEvGoBlock, 1)
			continue
		}
		// At least one timer pending. Sleep until then.
		timers.sleeping = true
		timers.sleepUntil = now + delta
		// 重置
		noteclear(&timers.waitnote)
		unlock(&timers.lock)
		// 使goroutine进入睡眠状态,直到notewakeup被调用,
		// 通过notewakeup 唤醒
		notetsleepg(&timers.waitnote, delta)
	}
}


golang使用最小堆(最小堆是满足除了根节点以外的每个节点都不小于其父节点的堆)实现的定时器。golang []*timer结构如下:

golang存储定时任务结构

addtimer在堆中插入一个值,然后保持最小堆的特性,其实这个结构本质就是最小优先队列的一个应用,然后将时间转换一个绝对时间处理,通过睡眠和唤醒找出定时任务,这里阅读起来源码很容易,所以只将代码和部分注释写出。

我的博客: 诺唯 | Noaway


有疑问加站长微信联系(非本文作者)

本文来自:知乎专栏

感谢作者:诺唯

查看原文:Golang定时器实现

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2569 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传