最近看了一些golang的time相关的内容,零零散散地凑点东西写写。

主要会有几点内容:

  1. time.Now()到底是不是系统调用;
  2. time.ticker等计时器是如何实现的;

然后有很多内容网上有很多文章其实讲得很清楚了,文中我可能会贴一些链接,然后简略的带过。

调用time.Now()到底有没有发生系统调用

系统调用是指用户态的进程向操作系统内核请求更高权限的服务,这些服务一般包括进程管理、内存管理、文件系统等。发生系统调用时需要进程从用户态切换到内核态,进行上下文切换,损耗很大。

系统时间也是由操作系统内核所管理的,之前一直以为time.Now()需要发生系统调用。包括之前看到一篇关于系统性能优化的博客,里面特意提到time.Now()为系统调用,大量调用time.Now()是潜在的优化项。例如在常规的业务系统中,日志打点等都会默认获取当前的时间,其提供的一个解决思路是在内存中维护一个时间,定时地进行更新(比如起一个goroutine以ms为interval去更新时间,当然精度会收到协程调度的影响),需要时间就去获取该时间,少部分需要精确时间的位置再去调用time.Now()。

那么time.Now()到底有没有进行系统调用呢?答案是没有。golang中使用了vdso的进行了优化。

vdso全称是virtual dynamic shared object,虚拟共享对象。vdso是linux内核提供的一种将内核函数暴露给用户空间的技术,其可以将不涉及安全的内核代码映射到用户空间。应用进程可以直接调用内核函数而不必进行用户态到内核态的切换,减少损耗。

更具体的内容可参见Go语言中调用time.Now()时有没有发生系统调用?这篇文章基于go 1.14.15进行了很详细的介绍。

time.ticker等计时器是如何实现的

golang中涉及到计时相关的有time.Ticker、time.Timer、time.Sleep()等,这里主要以time.Ticker为例进行介绍,其他底层实现类似。

func main() {
   ticker := time.NewTicker(time.Second)
   defer ticker.Stop()

   for {
      t := <-ticker.C
      log.Println(t)
   }
}

通常会以上面demo的方式来使用ticker,传入duration得到ticker对象,通过ticker持有的channel来感知计时行为。

Ticker的数据结构

点进NewTicker方法中可以看到Ticker对象持有两个字段,其中一个就是上面提到channel,另外一个是runtimeTimer。channel是用来通知上层计时行为的,runtimeTimer则是golang的runtime来管理计时器的真正对象。所以ticker就是在runtimeTimer上包了一层通信,我们接下来主要关注runtimeTimer。

runtimeTimer的结构如下,比较关键的字段其实就是上面NewTicker方法中赋值的几个字段。when表示计时器到时的绝对时间,是当前时刻+duration;f为到时间后的回调函数;arg为回调函数的第一个传参。

看下sendTime的实现就知道ticker在到时后的行为是向channel中非阻塞的塞入当前的时间。至于为什么是非阻塞的写入,golang是这样解释的:The ticker will adjust the time interval or drop ticks to make up for slow receivers 。

管理runtimeTimer

然后runtime到底是如何管理runtimeTimer的呢?去看startTimer具体做了什么。这里startTimer用了linkname的方式。其内部主要是addTimer,继续看addTimer做了什么。

addTimer中做的事情就比较具体了。首先该操作的过程中不能m和p不能分离。如果你不知道mp是什么,那应该先去看下GMP,这是golang最基本的调度模型。然后获取当前的p,上锁,将timer对象加入p中。

现在我们知道golang中是以p为单位来管理计时器的。p持有一个timer的切片,其实是以小顶的四叉堆的形式来组织所有的timer。在doaddtimer方法中我们会看到里面有上移的操作(类似二叉堆),这里我们不纠结这些细节。

在将timer加入相应的p的小顶四叉堆后,调用了wakeNetPoller方法,使得netpoller至少在timer的到期时间醒来。这里涉及到golang runtime中一个很重要的实现netpoller,简单提两句,我们在应用代码中做的同步的网络io实际在runtime层面都是异步的,runtime中利用多路复用技术来管理所有的网络io。

那么这里wakeNetPoller起到的是什么作用呢?现在golang中以小顶四叉堆的方式组织所有的timer,(在某些时刻,后面会说是什么时刻)不断地去拿堆顶的timer,如果current time >= timer.when就执行其callback;如果没到期的话会希望等待直到timer.when再去堆中查看。所以wakeNetPoller(when)其实就是起到了等待直到timer.when再去堆中查看的作用。

执行计时器

上面提到了golang中计时器的数据结构以及runtime是如何组织这些计时器,那么runtime是在什么时候去执行计时器呢?
runtime/time.go文件中提供了checkTimers/runtimer/runOneTimer三个方法。checkTimers方法中,如果当前p的timers长度不为0,就不断地调用runtimers。runtimes会根据堆顶的timer的状态判断其能否执行,如果可以执行就调用runOneTimer实际执行。这块整天逻辑非常简单,不做赘述,感兴趣可以自己看看。

所以现在的关键就是checkTimers什么时候被调用?搜索发现findrunnable/schedule/stealWork三个方法中被调用,稍微熟悉runtime的都知道这几个是goroutine的调度方法,所以真正执行timer是在goroutine调度时发生的。