golang定时器的精度

以 go1.23.3 linux/amd64 为例。

定时器示例代码:

package main

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

var ctx context.Context

func main() {
	timeout := 600 * time.Second
	ctx, _ = context.WithTimeout(context.Background(), timeout)
	deadline, _ := ctx.Deadline()
	fmt.Println("process start", time.Now().Format(time.DateTime))
	fmt.Println("ctx deadline", deadline.Format(time.DateTime))
	go func() {
		defer func() {
			fmt.Println("goroutine exit", time.Now().Format(time.DateTime))
		}()
		for {
			select {
			case <-ctx.Done():
				fmt.Println("ctx.Done", time.Now().Format(time.DateTime))
				return
			default:
				fmt.Println("do something start", time.Now().Format(time.DateTime))
				time.Sleep(60 * time.Second)
				fmt.Println("do something end  ", time.Now().Format(time.DateTime))
			}
		}
	}()
	time.Sleep(timeout + 10*time.Second)
	fmt.Println("process exit", time.Now().Format(time.DateTime))
}

定时器创建流程:

定时器的类型为:runtime.timer,新建定时器会调用runtime.newTimer函数。

runtime.newTimer函数会调用func (t *timer) maybeAdd(),在此函数中将定时器放入ts堆中:

func (t *timer) maybeAdd() {
	// Note: Not holding any locks on entry to t.maybeAdd,
	// so the current g can be rescheduled to a different M and P
	// at any time, including between the ts := assignment and the
	// call to ts.lock. If a reschedule happened then, we would be
	// adding t to some other P's timers, perhaps even a P that the scheduler
	// has marked as idle with no timers, in which case the timer could
	// go unnoticed until long after t.when.
	// Calling acquirem instead of using getg().m makes sure that
	// we end up locking and inserting into the current P's timers.
	mp := acquirem()
	ts := &mp.p.ptr().timers
	ts.lock()
	ts.cleanHead()
	t.lock()
	t.trace("maybeAdd")
	when := int64(0)
	wake := false
	if t.needsAdd() {
		t.state |= timerHeaped
		when = t.when
		wakeTime := ts.wakeTime()
		wake = wakeTime == 0 || when < wakeTime
		ts.addHeap(t)
	}
	t.unlock()
	ts.unlock()
	releasem(mp)
	if wake {
		wakeNetPoller(when)
	}
}

入堆:

// addHeap adds t to the timers heap.
// The caller must hold ts.lock or the world must be stopped.
// The caller must also have checked that t belongs in the heap.
// Callers that are not sure can call t.maybeAdd instead,
// but note that maybeAdd has different locking requirements.
func (ts *timers) addHeap(t *timer) {
	assertWorldStoppedOrLockHeld(&ts.mu)
	// Timers rely on the network poller, so make sure the poller
	// has started.
	if netpollInited.Load() == 0 {
		netpollGenericInit()
	}

	if t.ts != nil {
		throw("ts set in timer")
	}
	t.ts = ts
	ts.heap = append(ts.heap, timerWhen{t, t.when})
	ts.siftUp(len(ts.heap) - 1)
	if t == ts.heap[0].timer {
		ts.updateMinWhenHeap()
	}
}

timers堆为每P持有,保存P队列中协程定义的定时器。

// A timers is a per-P set of timers.
type timers struct {
	// mu protects timers; timers are per-P, but the scheduler can
	// access the timers of another P, so we have to lock.
	mu mutex

	// heap is the set of timers, ordered by heap[i].when.
	// Must hold lock to access.
	heap []timerWhen

	// len is an atomic copy of len(heap).
	len atomic.Uint32

	// zombies is the number of timers in the heap
	// that are marked for removal.
	zombies atomic.Int32

	// raceCtx is the race context used while executing timer functions.
	raceCtx uintptr

	// minWhenHeap is the minimum heap[i].when value (= heap[0].when).
	// The wakeTime method uses minWhenHeap and minWhenModified
	// to determine the next wake time.
	// If minWhenHeap = 0, it means there are no timers in the heap.
	minWhenHeap atomic.Int64

	// minWhenModified is a lower bound on the minimum
	// heap[i].when over timers with the timerModified bit set.
	// If minWhenModified = 0, it means there are no timerModified timers in the heap.
	minWhenModified atomic.Int64
}

type timerWhen struct {
	timer *timer
	when  int64
}

创建定时器堆栈如图:

定时器触发流程:

timers堆的定时器通过func (ts *timers) run(now int64) int64出堆并运行。

而检查是否有定时器到期是通过函数func (ts *timers) check(now int64) (rnow, pollUntil int64, ran bool)中的func (ts *timers) wakeTime() int64进行的。

check函数和wakeTime函数的调度时机在runtime/proc.go文件中多处存在,如runtime.findRunnable()、runtime.stealWork(now int64)、runtime.schedule()等。

这种依赖协程调度、系统调用等触发的定时器检查,延迟时间最多可达到func sysmon()协程的间隔时间10ms。

触发定时器堆栈如图:

另外在新建定时器时,也会检查timers堆顶部的定时器剩余时间,如果已经到期也会立刻通过runtime.wakeNetPoller(when int64)触发runtime.netpoll(delay int64)返回,检查是否存在可处理的event,然后进行timers堆的定时器check。

定时器精度小结:

golang内置的Timer定时器维护在用户态,比较轻量,依赖协程调度、系统调用、event等来触发时间到期检查,延迟在10ms以内,精度不高。

定时器的观测:

修改源码创建多个ctx定时器:

package main

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

var ctx context.Context

func main() {
	timeout := 300 * time.Second
	ctx, _ = context.WithTimeout(context.Background(), timeout)
	ctx, _ = context.WithTimeout(ctx, 180*time.Second)
	deadline, _ := ctx.Deadline()
	fmt.Println("process start", time.Now().Format(time.DateTime))
	fmt.Println("ctx deadline", deadline.Format(time.DateTime))
	go func() {
		defer func() {
			fmt.Println("goroutine exit", time.Now().Format(time.DateTime))
		}()
		for {
			select {
			case <-ctx.Done():
				fmt.Println("ctx.Done", time.Now().Format(time.DateTime))
				return
			default:
				fmt.Println("do something start", time.Now().Format(time.DateTime))
				time.Sleep(5 * time.Second)
				fmt.Println("do something end  ", time.Now().Format(time.DateTime))
			}
		}
	}()
	time.Sleep(timeout + 10*time.Second)
	fmt.Println("process exit", time.Now().Format(time.DateTime))
}

dlv调试:

1、查看当前的定时器数量:

p runtime.allp[1].timers.heap 

2、查看每个定时器的超时时间:

p (runtime.allp[1].timers.heap[0].when - time.startNano)/int64(time.Second)
p (runtime.allp[1].timers.heap[0].when - time.startNano)/int64(time.Second)
p (runtime.allp[1].timers.heap[0].when - time.startNano)/int64(time.Second)

3、调用其中一个定时器的回调函数:

call runtime.allp[1].timers.heap[0].timer.arg.(func())()

4、查看控制台输出:

共有3个定时器,分别是ctx的2个和主协程的time.Sleep,其中timers堆顶是180s的定时器。

在手工调用timers堆顶定时器的回调函数后,提前收到ctx.Done通知,程序提前退出。

如图:

cancelCtx的父子关系:

继续上面的例子:

1、查看ctx两个定时器的回调函数是否一致:

p runtime.allp[1].timers.heap[0].timer.arg
p runtime.allp[1].timers.heap[1].timer.arg

2、查看父子cancelCtx变量内容:

#子ctx
p ctx
#父ctx
p ctx.cancelCtx.Context

3、观测结果说明:

父子cancelCtx回调函数内部引用的外部变量context.timerCtx并不相同。

父子cancelCtx之间是嵌套关系,子嵌套(继承)父。

最终使用的ctx为子ctx,子ctx的任一层父ctx的超时都会导致子ctx退出。

如图:

父子cancelCtx的嵌套关系通过函数func (c *cancelCtx) propagateCancel(parent Context, child canceler)完成:

// propagateCancel arranges for child to be canceled when parent is.
// It sets the parent context of cancelCtx.
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
	c.Context = parent

	done := parent.Done()
	if done == nil {
		return // parent is never canceled
	}

	select {
	case <-done:
		// parent is already canceled
		child.cancel(false, parent.Err(), Cause(parent))
		return
	default:
	}

	if p, ok := parentCancelCtx(parent); ok {
		// parent is a *cancelCtx, or derives from one.
		p.mu.Lock()
		if p.err != nil {
			// parent has already been canceled
			child.cancel(false, p.err, p.cause)
		} else {
			if p.children == nil {
				p.children = make(map[canceler]struct{})
			}
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
		return
	}

	if a, ok := parent.(afterFuncer); ok {
		// parent implements an AfterFunc method.
		c.mu.Lock()
		stop := a.AfterFunc(func() {
			child.cancel(false, parent.Err(), Cause(parent))
		})
		c.Context = stopCtx{
			Context: parent,
			stop:    stop,
		}
		c.mu.Unlock()
		return
	}

	goroutines.Add(1)
	go func() {
		select {
		case <-parent.Done():
			child.cancel(false, parent.Err(), Cause(parent))
		case <-child.Done():
		}
	}()
}

--end--

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值