Go 墙上时间和递增时间
潘忠显 / 2024-07-09
在计算机系统中,有两个常见的时间:
-
墙上时间(Wall-Clock Time):系统的实际时间,通常是由操作系统维护和更新的时间。它受到日光节约时间(DST)的影响,也可能会被用户手动调整,例如通过手动更改系统时钟或网络时间协议(NTP)同步。
-
递增时间(Monotonic Time):相对于系统启动时钟的一个单调递增的时间计数器。它不受系统时间调整的影响,因此在处理需要精确时间间隔或计时的应用程序中特别有用。
在多数情况下,墙上时间用于记录事件的实际发生时间,例如文件的创建时间、日志的时间戳等。而递增时间用来计算时间精确的时间间隔,不能受到系统时间调整干扰。
在 Linux 系统上,我们可以通过 man clock_gettime
可以看到系统调用 clock_gettime()
可以通过接收不同的参数,获得不同的时间,使用 CLOCK_REALTIME
获得墙上时间,而使用 CLOCK_MONOTONIC
获得递增时间。
Go 语言中,我们要如何获得我们想要的时间呢?
在 标准库 time 文档中,有提到说 time.Now 同时包含了这两个时间:
Rather than split the API, in this package the Time returned by time.Now contains both a wall clock reading and a monotonic clock reading; later time-telling operations use the wall clock reading, but later time-measuring operations, specifically comparisons and subtractions, use the monotonic clock reading.
time.Time 结构体
我们看一下 Time
结构体:
- wall 主要用于存储墙上时间,但设计上有细节需要注意:
- 1bit 存储是否有递增时间
- 33bit可以用于存时间戳的秒,跨度272年左右
- 30bit存储时间戳的纳秒,有效的只能是 0 ~ 1e9-1
- ext 完全依赖 wall 中 hasMonotonic bit
- 为 0 时存储时间戳,而 wall 中的33bit就置0,ext 存储从公元1年1月1日到现在的时间戳
- 为 1 时,存储进程启动之后,递增时间纳秒值,wall 的中间 33bit 则存1885到现在的时间戳
为了更清楚地展示,我这里画出了 hasMonotonic 分别为 0 和 1 的分布:
【注意】上边提到的、以及注释中的 hasMonotonic
指的是 wall
中的一个 bit,要跟 time.hasMonotonic
这个常量区分开,后者是用作取 wall 中 bit 的掩码。所以你到处会看到 t.wall&hasMonotonic != 0
类似的判断。
time.Now() 函数
接下来再看一下 Now()
函数,他最终返回一个 Time
结构,可以看到这里会调用内部的 now()
函数,获得墙上时间的秒、纳秒,以及递增时间 mono
,最后会将这三个值构建一个 Time
结构对象。
注意这里的细节,wall 拼接跟前面我们描述的规则一致:将标志位置为 1
,填充两个时间戳,并且使用 ext
存储 mono
time.Unix(sec, nsec) 函数
我们也比较常用将时间戳转换成时间,会用到函数 time.Unix(sec, nsec)
,也会返回一个 Time
对象。
但我们通过源码可以看到,他跟上边提到的 time.Now()
不同,他不会使用 hasMonotonic
,并且直接将时间戳的秒填充到 ext:
time.Time.Unix() 函数
我们自己写代码,经常用诸如 int(time.Now().Unix())
的代码来获得当前时间戳。
这里 Unix()
函数代码如下,使用的就是墙上时间。
【注意】这里出现了 t.wall&hasMonotonic != 0
的判断,根据不同的场景,从不同的位置获取时间戳值。
time.Time.Add() 函数
而我们也经常用 Add()
函数,比如在当前时间增加几秒,以控制超时或者其他操作。
而这个 Add()
函数本质会调用到 addSec()
,后者可以判断原来的 Time 到底是有单调时间的,还是没有单调时间:
- 如果有单调时间,就在单调时间上增加 Duration
- 如果没有单调时间,就在墙上时间上增加 Duration
- 特别的,如果有单调时间且增加之后越界了,会将有单调时间转换成无单调时间的格式。这里
t.wall &= nsecMask
会只保留 nsec 的30bit,其他的bit会被抹成0
时间比较函数
时间比较函数不涉及到加减超过表达范围,会直接依据原来的类型进行比较。
只有当比较的两个时间都具有单调时间的时候,使用单调时间比较,否则都使用墙上时间比较:
小结
通过阅读 time 包的源码,我们搞清楚了 Go 是如何通过一个 Time
结构来同时维护着墙上时间和单调时间两个时间的。
- time.Now() 等函数可以获取到有单调时间的
- time.Unix() 等函数只能获得无单调时间,只有墙上时间的
- 无论有无单调时间均可进行比较,但是两个都有单调时间,则优先比较单调时间
- 一些情况下,有单调时间的可以转化成无单调时间的
这里将以上转换关系汇总成一个图,方便大家理解。