1 标准时钟类型
1.1 理解 C++ 的 clock
人类理解的时间和使用的时间其实是不一致的,人类能感知时间的流逝,但是对时间的绝对 0 点的认识依然停留在大爆炸理论上。大爆炸发生的时刻是否就是时间的绝对 0 点,在那之前有没有时间?人们不得而知,但是这并不影响人类使用时间。不知道大家意识到没有,我们说一个时间点的时候,其实隐含了两个状态,一个是时间的相对起点,另一个是相对这个起点的时间间隔。比如我们说公元 2012 年 9 月 16 日 15 时 28 分 16 秒是一个确定的时间点,它暗示了这个时间点的相对起点是格里历确认的公元元年(也有说法是公元 1 年),以及相对于这个时间点的时间偏移是: 2011 年 + 9 月+16 天 + 15 小时 + 28 分 + 16 秒。实际上这个起点的确定只是基督教的僧侣们推算的耶稣诞生时间,并不一定有什么科学论证,但是并不影响大家在这个时钟体系内愉快地玩耍。
我们国家在古代使用的是年号+偏移的方式,每个新皇帝登基就创建一个年号,比如我们记载鸦片战争发生在道光二十年。这个时间点的相对起点是道光元年,偏移量是 19 年。这种使用时间的方式在遇到分裂时期,同时有多个皇帝存在的时候就非常烧脑了,孙权凭啥用刘备的年号?这可能就是中国人历来都追求一个统一的国家的原因吧,至少计时方面简单一点,毕竟,不同地方的人约个饭还要换算时间,那可真是“活不易”啊。
计算机系统内也有不同的时间体系,但是也遵循人类使用时间的原则,它使用一个相对起点和距离此相对起点的时间间隔来表达时间点。前面介绍 C 语言的时间处理时介绍的 time_t
类型时间,就是从格林威治标准时间 1970 年 1 月 1 日 0 时 0 分 0 秒到现在历时的秒数。那么计算机内不同的系统表达时间是否都用这个时间点作为相对时间的起点?当然不是,不仅相对起点不一样,时间间隔的表达也不一样,比如记录文件的创建时间和修改时间,用的相对时间起点是格林威治标准时间 1601 年 1 月 1 日 12 时 0 分 0 秒,时间间隔计算是以 100 纳秒(nanosecond)为单位的计数器。
除了时间纪元的起点不同,不同的系统内时间间隔的计数精度也不一样,这就造成了计算机系统中时间记录方式的差异。为了区分这种差异,C++ 11 开始引入了系统时钟类型的概念,不同的时钟,对时间的理解和计算也不一样。C++ 的 chrono 库确实让很多老程序员都目瞪口呆,这些都是专业的时间库,比如天文算法库中才用到的时间概念,硬生生被整成了老少都不得不“咸宜”的 C++ 基础知识。不过,如果理解了上面人类使用时间的例子,也就比较容易理解 C++ 的时钟的概念了。一种 clock,其实就代表一种时间表达的方式,包括相对时间起点的定义和时间间隔的定义。
1.2 system_clock
system_clock 代表一种系统级的实时时钟,因为是实时,所以它不能保证线性单调。不能保证线性单调的原因是因为它会随着系统时间被调整而改变,所以不能保证后取的时间值一定大于之前取的时间值。C++ 11 对 system_clock 的时间纪元起点和计时精度都没有定义,C++ 20 因为要引入 “file_clock”,为了明确两种时钟的差异,就顺便明确了 system_clock 的时间纪元起点是格林威治标准时间 1970 年 1 月 1 日 0 时 0 分 0 秒。至于计时精度,则因系统而已,在现代计算机系统上,普遍能达到 100 纳秒的精度。但是在一些老的嵌入式系统中,有可能达不到这么高的精度。
所以,总结一下,system_clock 是系统级别的实时时钟,不保证线性单调,时间纪元起点是格林威治标准时间 1970 年 1 月 1 日 0 时 0 分 0 秒,计时精度不确定,最好情况是 100 纳秒的级别。system_clock 有个全局常量system_clock::is_steady
,用于表示当前系统的 system_clock 是否是稳定时钟,大多数系统中,这个常量的值都是 false
。
system_clock 有三个静态成员方法,system_clock::now()
返回当前时间,返回值是一个时间点类型的变量。system_clock可以和 C 的time_t
类型能互相映射,所以 system_clock 的三个静态成员方法中的另外两个都是和time_t
进行转换计算用的。system_clock::to_time_t()
用于将一个 system_clock 类型的时间点转换成time_t
类型的整数值(单位是秒),下面的代码展示了 system_clock 类型的时间点与 C 语言的时间方法如何协作使用:
using namespace std::chrono;// The old way
std::time_t oldt = std::time(nullptr);// The new way
std::time_t newt = system_clock::to_time_t(system_clock::now());
system_clock::from_time_t()
方法将一个time_t
类型的时间转换成一个 system_clock 类型的时间点,是system_clock::to_time_t()
方法的逆运算。这里需要注意,大多数系统上的 system_clock 都具有毫秒、甚至微妙以上的精度,所以转换成以秒为单位的 time_t 的时候会丢失时间精度。
system_clock 可以满足大多数程序对时间的需求,比如展示当前时间,或者记录某个事件的发生事件,等等。
1.3 steady_clock
steady_clock 代表一种线性单调的时钟,它只能沿着时间增加的方向线性增长,不能被用户调整。这种类型的时钟所表示的时间,不能转换成我们一般理解的年、月、日时间,steady_clock 代表的时间点和 system_clock 代表的时间点具有完全不同的意义。在不同的系统上,编译器实现 steady_clock 的方式也不一样,比如在 windows 系统上,普遍使用 QueryPerformanceCounter()
API 来获取时间点的计数。另外,steady_clock 的时间纪元起点在标准里没有定义,如果是使用 QueryPerformanceCounter()
,则这个 0 起点就是系统启动的那个时间点。
steady_clock 有一个常量steady_clock::is_steady
,毫无疑问,这个值是 true。steady_clock 还有一个静态方法steady_clock::now()
,用于获取这种类型时钟的当前时间点。如果要统计两个时间点之间的时间间隔,最好用 steady_clock,因为它能保证线性单调。如果用 system_clock,有可能出现晚出现的时间点在时间值上早于先出现的时间点的情况。下面的例子是统计某个方法执行时间的典型例子:
using namespace std::chrono;steady_clock::time_point startTime = steady_clock::now();
//do something
steady_clock::time_point endTime = steady_clock::now();
auto period = duration_cast<milliseconds>(endTime - startTime);
std::cout << "do something need: " << period << std::endl;
steady_clock::time_point
实际上是std::chrono::time_point<std::chrono::steady_clock>
的一个别名,使用这个别名能够让代码短一点。
很显然,steady_clock 不用于人类理解的时间的表达,不要以为它“稳定”就是好时间。steady_clock 只是确保你后一次调用 now 获得的时间一定在前一次获得的时间之后,system_clock 不具有这个稳定性,因为 system_clock 随系统的时间而定,你可以随意将系统功能时间修改为一天之前的某个时间。
1.4 high_resolution_clock
high_resolution_clock 代表系统上每个 tick 颗粒度最小的时钟,C++ 标准没有明确这种时钟的具体实现,编译器厂商可以自由发挥。它可能是 system_clock 或 steady_clock 的别名,也可能是编译器厂商根据某个特殊的系统实现的一种时钟。根据经验,gcc 编译器实现的标准库把它当作 system_clock ,MSVC 的 CRT 把它当作 steady_clock ,至于 clang’s,据说是可以配置使用 system_clock 还是 steady_clock 。
如果 high_resolution_clock 代表的是 steady_clock,那么它是稳定时钟,即线性单调的,如果 high_resolution_clock 代表的是 system_clock,那么它是不稳定时钟,如果不确定的话,可以使用常量 high_resolution_clock::is_steady
进行判断,如果这个常量的值是 true,就说明 high_resolution_clock 是个稳定时钟,如果是 false,就说明 high_resolution_clock 是不稳定时钟。
对于大多数电脑环境上的 high_resolution_clock,C++ 标准建议尽量不要使用,如果想作为墙钟使用,就用 system_clock ,如果是测试时间间隔,就用 steady_clock。这个高精度时钟可能会在一些特殊的系统中使用,比如某些航天系统,它的设备上往往有更高精度的时钟源。
1.5 file_clock(C++ 20)
实际上,有些编译器很早就提供了 file_clock,C++ 标准没有明确 file_clock 的时间纪元起点,但是不少编译器提供的实现都默认将时间纪元起点定为格林威治标准时间 1601 年 1 月 1 日 12 时 0 分 0 秒,这也是目前主流操作系统记录文件时间的依据。file_clock 也有一个常量 is_steady,用于判断这个时钟是否稳定,不同的编译器提供的标准库有不同的实现,使用之前判断一下是比较稳妥的办法。
前面提到过,操作系统记录文件时间使用的是一个 64 位的计数值,这个值是从1601 年 1 月 1 日 12 时 0 分 0 秒到现在经过的以 100 纳秒位单位的计数值。我猜大部分编译器都是用这个计数来对齐 file_clock 的实现,否则标准定义的将一个 file_clock 的时间点转换为 system_clock 时间点或 time_t 类型的系统时间的接口就很难实现。
做过文件时间处理的读者都理解文件时间处理的麻烦之处,将文件时间展示给用户看的时候,要转换成系统时间。在 C++ 20 之前,做这个转换要么使用操作系统提供的 API 接口,要么自己根据时间起点的偏差做一通修正计算,两个计数器的时间精度不一样也给这种换算添加一层复杂度。由此可知 file_clock 的意义,不仅让文件时间有了更好的类型表达(比一个 unsigned long long 类型的计数器有更好的信息表达),还提供了文件时间与系统时间的转换方法。to_sys() 和 from_sys() 是 file_clock 类的两个静态成员方法,也是模板方法,用于 system_clock 时间点和 file_clock 时间点之间互相转换:
template <class Duration>
static std::chrono::sys_time<Duration> to_sys(const std::chrono::file_time<Duration>& t);template <class Duration>
static std::chrono::file_time<Duration> from_sys(const std::chrono::sys_time<Duration>& t);
同时,to_utc() 和 from_utc() 是 file_clock 类的另外两个静态成员方法,也是模板方法,用于 utc_clock 时间点和 file_clock 时间点之间互相转换:
template <class Duration>
static std::chrono::utc_time<Duration> to_utc(const std::chrono::file_time<Duration>& t);template <class Duration>
static std::chrono::file_time<Duration> from_utc(const std::chrono::utc_time<Duration>& t);
顺便说一下, to_sys() 、 from_sys() 、to_utc() 和 from_utc()在 C++ 20 中被定义为可选内容,你的编译器可能支持,也可能不支持,视情况而定。
1.6 utc_clock(C++ 20)
本文的第一部分介绍过 UT 时间和 UTC 时间的关系,UT 就是普遍使用的,基于天体测量和计算的世界时,UTC 则是原子钟代表的恒定线性时钟。C++ 20 明确是 system_clock 的时间纪元起点是 1970 年 1 月 1 日 0 时 0 分 0 秒,也就意味着 system_clock 记录的就是目前普遍使用的世界时,虽然它和 UTC 时间只存在最大 0.9 秒的差别,但确实是两个不同的 clock。
国际计量局规定当两个时间差别超过正负 0.9 秒时就对 UTC 时间进行正负闰秒修正,使得 UTC 时间与 UT 时间保持近似的一致。由于地球自转在变慢,按照 UT 时间地球自转一周是 24 小时的话,每一秒代表的时间长度实际上是变短了,所以从 1972 年到现在所进行的 27 次闰秒修正都是正闰秒。我们可以通过一个简单的转换计算确认这一点:
using namespace std::chrono;auto utc_now = utc_clock::now();
auto utc2sys = utc_clock::to_sys(utc_now); //转换成 system_clock 时间
auto utc_ms = duration_cast<milliseconds>(utc_now.time_since_epoch()); //转换成 ms 单位时间间隔
auto sys_ms = duration_cast<milliseconds>(utc2sys.time_since_epoch()); //转换成 ms 单位时间间隔
std::cout << (utc_ms - sys_ms) << std::endl; //输出 27000ms
输出结果是 27000ms,由此可知,将 utc_clock 的时间点转换成 system_clock 的时间点的时候,会将当前已经插入的闰秒考虑进去,时钟在计算从时间起点到现在的时间间隔时存在 27 秒的时间差,这就是因为插入闰秒造成的。C++ 20 还提供了一个 get_leap_second_info()
函数,用这个函数可以获取一个 utc_clock 的时间点的闰秒情况,这个函数返回一个类型是 leap_second_info
的的信息:
struct leap_second_info {bool is_leap_second;std::chrono::seconds elapsed;
};
is_leap_second
表示当前是否需要执行闰秒的插入,elapsed
记录从时间纪元起点到现在一共插入了多少个闰秒。通过这个函数获取的闰秒时间与前面计算的结果一致:
auto utc_now = utc_clock::now();
auto leapinfo = get_leap_second_info(utc_now);
std::cout << leapinfo.elapsed << std::endl; //输出 27s
闰秒给通信、航天、电子等对时间精度和连续性要求都很高的领域带来了巨大的麻烦,同时也增加了软件领域做时间处理的复杂度。首先你需要带一个闰秒数据库才能完成转换,其次,由于闰秒没有规律,仅使用现有的数据库做计算是不严谨的,转换算法必需要联网获取最新的闰秒数据库。最后,软件开发人员在设计上必需考虑对 09:59:60 这种情况的支持,否则就可能弄个溢出出来。所以,在 2022 年,国际授时与计量组织召开了一次由各国政府和科学家代表组成的讨论会,确定将在 2035 年取消闰秒。取消闰秒就意味着我们使用的世界时和 UTC 时间从此分道扬镳,各算各的,由此会带来什么问题就只能静观其变了。
1.7 tai_clock(C++ 20)
tai_clock 代表的是国际原子时(International Atomic Time),它的时间纪元起点是 格林威治标准时间 1958 年 1 月 1 日 0 时 0 分 0 秒,并且 TAI 时间比 UTC 时间领先 10 秒钟,也就是说,TAI 时间的 1958 年 1 月 1 日 0 时 0 分 0 秒,相当于 UTC 时间的 1957 年 12 月 31 日 23 时 59 分 50 秒。但是 TAI 时间不考虑闰秒,而 UTC 时间到 2017 年 12 月,已经累积闰了 27 秒,所以目前 TAI 时间领先 UTC 时间 37 秒。
一般软件开发很少会用到 TAI 时间,可能在一些天文计算方面会用到。除了常量 is_steady,tai_clock 类还提供了三个静态方法,分别是now()
、 to_utc()
和 from_utc()
,用于获取当前时间点和与 UTC 时间的互相转换,使用方法很简单,这里不再赘述。
1.8 gps_lock(C++ 20)
gps_lock 代表全球定位系统(Global Positioning System)使用的时钟,它的时间纪元起点是 UTC 时间 1980 年 1 月 6 日 0 时 0 分 0 秒,因为 gps_lock 也不计算闰秒,所以截至到 2017 年 12 月,它领先 UTC 时间 18 秒(1980 年到 2017 年,UTC 时间闰了 18 秒),但是落后 TAI 时间 19 秒。
做地理信息系统的开发人员可能会用到 gps_lock ,gps_lock 类除了常量 is_steady,还提供了三个静态方法,分别是now()
、 to_utc()
和 from_utc()
,用于获取当前时间点和与 UTC 时间的互相转换,使用方法很简单,这里不再赘述。
1.9 local_t(C++ 20)
local_t 其实不是一个真正意义的时钟类型,它只是一个类型占位符,用于类型上区分 system_clock 所表示的系统时间。系统用这个类型定义了一组时间点的别名,即本地时间:
template<class Duration>
using local_time = std::chrono::time_point<std::chrono::local_t, Duration>;
using local_seconds = local_time<std::chrono::seconds>;
using local_days = local_time<std::chrono::days>;
引入 local_t 时钟类型的原因是利用它定义 local_time 类型的时间点,这是 C++ 20 引入的时区处理中区别一个时间是系统时间还是本地时间的关键点。时区和日历功能中的相关函数在返回本地时间时可使用 local_time 类型的时间点,以便相关的一些 type_traits 检查能识别出这个时间点是本地时间还是系统时间。关于 local_t 的使用,将在本系列的时区和日历的部分具体介绍。
2 时钟转换
2.1 clock_cast 函数(C++ 20)
在使用 C++ 11 的时间点的时候,最麻烦的事情就是不同时钟类型的时间点之间的换算,很多情况下不得不将时间点还原成计数器,然后利用时间纪元起点的差值修正计数器,然后将修正后的计数器还原成对应时钟类型的时间点,在 C++ 20,就不需要这么麻烦了,因为它提供了 clock_cast() 方法,这是一个模板方法:
template <class Dest, class Source, class Duration>
auto clock_cast(const std::chrono::time_point<Source, Duration>& t);
将一个 utc_clock 类型的 time_point 转换成 system_clock 类型的 time_point,可以用 utc_clock::to_sys() 函数,也可以用 clock_cast()
,二者结果是一致的:
auto utc_now = utc_clock::now();
auto utc2sys1 = utc_clock::to_sys(utc_now);
auto utc2sys2 = clock_cast<system_clock>(utc_now);assert(utc2sys2 == utc2sys1);
另一个常使用 clock_cast()
的场景是文件时间处理,众所周知,文件时间无论是时间纪元起点还是时间间隔类型都与系统时间不一样,当需要将一个文件系统获得的时间点转换成系统时间,并显式给用户的时候,常用 clock_cast()
:
auto ftime = file_entry.last_write_time(); //得到文件时间
auto sys_time = clock_cast<system_clock>(ftime); //转成系统时间
auto ltime = current_zone()->to_local(sys_time); //current_zone() C++ 20
std::cout << ltime << std::endl; //以本地时间显式文件时间
2.2 clock_time_conversion traits 类(C++ 20)
C++ 20 还提供了一个名为 clock_time_conversion 的traits 类,用于不同时钟类型的时间点的转换,这个类的原型如下:
template< class Dest, class Source >
struct clock_time_conversion {};
这个类的 operator() 接受一个 std::chrono::time_point<Source, Duration> 类型的时间点作为参数,并返回 std::chrono::time_point<Dest, Duration> 类型的转换结果。下面的代码演示了如何用 clock_time_conversion 将一个 utc_clock 时钟类型的时间点转换成 system_clock 时钟类型的时间点:
auto utc_now = utc_clock::now();clock_time_conversion<system_clock, utc_clock> conv;
//转换成 system_clock 时间
auto utc2sys = conv(utc_now);
3 其他
为了判断一个对象是否是时钟类型,C++ 20 提供了 is_clock 泛型类型,借助于模板偏特化机制,提供编译期间的类型判断,可以这样判断一个类型 T 是否是时钟类型:
if(is_is_clock<T>::value) {
...
}
或者直接使用 is_clock_v,它是这样定义的:
template< class T >
inline constexpr bool is_clock_v = is_clock<T>::value;
参考资料
[1] https://en.cppreference.com/w/cpp/chrono
[2] https://en.cppreference.com/w/cpp/chrono/file_clock
[3] https://en.cppreference.com/w/cpp/chrono/clock_time_conversion
[4] Bjarne Stroustrup. The C++ Programming Language, Fourth Edition. Pearson. 2013
[5] Nicolai M. Josuttis, C++20 - The Complete Guide, http://leanpub.com/cpp20’
[6] ISO/IEC 14882:2014 - 2020
[7] https://en.cppreference.com/w/cpp/chrono/clock_cast
关注作者的算法专栏
https://blog.csdn.net/orbit/category_10400723.html
关注作者的出版物《算法的乐趣(第二版)》
https://www.ituring.com.cn/book/3180