C++11中的时间库std::chrono(引发关于时间的思考)

时间都去哪了?还没好好感受年轻就…


前言

时间是宝贵的,我们无时无刻不在和时间打交道,这个任务明天下班前截止,你点的外卖还有5分钟才能送到,那个程序已经运行了整整48个小时,既然时间和我们联系这么紧密,我们总要定义一些术语来描述它,像前面说到的明天下班前、5分钟、48个小时都是对时间的描述,程序代码构建的程序世界也需要定义一些术语来描述时间。

今天要总结学习的是 std::chrono 库,它是 C++11 标准时从 boost 库中引入的,其实在 C++ 中还有一种 C 语言风格的时间管理体系,像我们常见的函数 time()clock()localtime()mktime() 和常见的类型 tmtime_tclock_t 都是 C 语言风格的时间管理体系。

std::chrono 这个库之前接触的不多,C++20 标准都出了,C++11 引入的这个库还没怎么用过,整天和 time()localtime()tm 打交道,最近工作中换了项目,代码中出现了 std::chrono 的使用,是时候好好学习总结一下了。

chrono 的概况

  • 头文件 #include <chrono>
  • 命名空间 std::chrono

这个库从 C++11 引入标准之后,每个版本都有所修改,不过核心内容变化不是太大,他定义了三种主要类型,分别是 durationsclockstime points,以及围绕这些类型的一些工具函数和衍生的定义。

chrono 的核心内容

duration

这个模板类用来表示时间间隔,我们知道时间的基本单位是秒,这个类的对象所表示的时间间隔也是以秒为单位的,它的定义如下:

1
2
template<class Rep, class Period = std::ratio<1>>
class duration;

Rep 表示一种数值类型,用来描述周期 Period 的数值类型,比如可以是 intfloat 等,而 Period 的类型是 std::ratio,同样是一个模板类,实际表示的是一个有理数,像100、0、1/1000(千分之一)等等。

std 这个命名空间下有很多已经定义好的有理数,可以举几个常见的头文件 <ratio> 中的例子:

1
2
3
4
5
6
7
8
nano    std::ratio<1, 1000000000>   // 十亿分之一
micro std::ratio<1, 1000000> // 百万分之一
milli std::ratio<1, 1000> // 千分之一
centi std::ratio<1, 100> // 百分之一
deci std::ratio<1, 10> // 十分之一
deca std::ratio<10, 1> // 十
hecto std::ratio<100, 1> // 百
kilo std::ratio<1000, 1> // 千

比如我们想定义一个整数类型的100秒的时间间隔类型可以使用:

1
typedef std::chrono::duration<int, std::ratio<100,1>> my_duration_type;

当然也可以简写成:

1
typedef std::chrono::duration<int, std::hecto> my_duration_type;

如果我们想定义一个整数类型1分钟的时间间隔类型可以写成:

1
typedef std::chrono::duration<int, std::ratio<60,1>> my_minute_type;

因为这种时、分、秒的时间表示在代码逻辑中很常用,所有在 std::chrono 命名空间下已经定义好了一些时间间隔类型:

1
2
3
4
5
6
std::chrono::nanoseconds    duration</*signed integer type of at least 64 bits*/, std::nano>
std::chrono::microseconds duration</*signed integer type of at least 55 bits*/, std::micro>
std::chrono::milliseconds duration</*signed integer type of at least 45 bits*/, std::milli>
std::chrono::seconds duration</*signed integer type of at least 35 bits*/>
std::chrono::minutes duration</*signed integer type of at least 29 bits*/, std::ratio<60>>
std::chrono::hours duration</*signed integer type of at least 23 bits*/, std::ratio<3600>>

另外还有一个很重要的成员函数 count(),用来获得指定的时间间隔对象中包含多少个时间周期,接下来可以写个例子理解一下,我们用 duration 这个模板类来表示一下5分钟和12小时,看看他应该怎么使用,对于5分钟你可以看成是 5 个 1 分钟或者 1 个 5 分钟,或者更变态你可以看成 2.5 个 2 分钟,而 12 小时一般会看成是 12个 1 小时,你当成 0.5 个 1 天也是可以的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <chrono>
#include <iostream>
int main()
{
// 以下为5分钟表达
std::chrono::minutes minute1{5}; // 5个1分钟
std::chrono::duration<int, std::ratio<5*60, 1>> minute2{1}; // 1个5分钟
std::chrono::duration<double, std::ratio<2*60, 1>> minute3{2.5}; // 2.5个2分钟

std::cout << "minutes1 duration has " << minute1.count() << " ticks\n"
<< "minutes2 duration has " << minute2.count() << " ticks\n"
<< "minutes3 duration has " << minute3.count() << " ticks\n";

// 一下为12小时表达
std::chrono::hours hours1{12}; // 12个1小时
std::chrono::duration<double, std::ratio<60*60*24, 1>> hours2{0.5}; // 0.5个1天

std::cout << "hours1 duration has " << hours1.count() << " ticks\n"
<< "hours2 duration has " << hours2.count() << " ticks\n";

// 使用 std::chrono::duration_cast<T> 将分钟间隔转化成标准秒间隔
std::cout << "minutes1 duration has " <<
std::chrono::duration_cast<std::chrono::seconds>(minute1).count() << " seconds\n";
}

上述代码中还使用了 std::chrono::duration_cast<T>() 函数,用于各种时间间隔的换算,运行结果如下:

1
2
3
4
5
6
minutes1 duration has 5 ticks
minutes2 duration has 1 ticks
minutes3 duration has 2.5 ticks
hours1 duration has 12 ticks
hours2 duration has 0.5 ticks
minutes1 duration has 300 seconds

clock

从名字可以看出这个类叫做时钟,时钟是用来看时间和计时的,常用的两个类是 system_clocksteady_clock,在 C++20 标准中又加入了多种内容,现在我们先来看看这两个常用类。

从这一部分开始类的定义让人有些迷糊,其实 clock 引用了 std::chrono::duration 和后面要说的 std::chrono::time_point, 而 std::chrono::time_point 又引用了 std::chrono::duration 和现在要讲的 std::chrono::system_clockstd::chrono::steady_clock,如果只看定义很容易被绕晕,所以还是先做个练习实验一下。

system_clock

这个类被称为系统内时钟,当修改系统时钟时可能会改变其单调递增的性质,静态成员函数有 now()to_time_t()from_time_t() 三个,关于它的单调性被修改举个例子,一般认为时间一直是递增的,但是当你现在调用一次函数 now(),然后把时间往过去调1天,然后再调用 now() 函数,就会发现新得到的时间“变小”了。

也因为这样它会受到 NTP(Network Time Protocol,网络时间协议)的影响,但是不会受时区和夏令时的影响(其实很多国家早就废除夏令时了)。

下面写个例子练习一下,例子中使用了 now()to_time_t()from_time_t() 三个函数,不清楚的时候可以对照一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <chrono>
#include <iostream>
int main()
{
std::chrono::duration<int, std::ratio<60*60*24> > one_day(1);

// 根据时钟得到现在时间
std::chrono::system_clock::time_point today = std::chrono::system_clock::now();
std::time_t time_t_today = std::chrono::system_clock::to_time_t(today);
std::cout << "now time stamp is " << time_t_today << std::endl;
std::cout << "now time is " << ctime(&time_t_today) << std::endl;


// 看看明天的时间
std::chrono::system_clock::time_point tomorrow = today + one_day;
std::time_t time_t_tomorrow = std::chrono::system_clock::to_time_t(tomorrow);
std::cout << "tomorrow time stamp is " << time_t_tomorrow << std::endl;
std::cout << "tomorrow time is " << ctime(&time_t_tomorrow) << std::endl;


// 计算下个小时时间
std::chrono::system_clock::time_point next_hour = today + std::chrono::hours(1);
std::time_t time_t_next_hour = std::chrono::system_clock::to_time_t(next_hour);
std::chrono::system_clock::time_point next_hour2 = std::chrono::system_clock::from_time_t(time_t_next_hour);

std::time_t time_t_next_hour2 = std::chrono::system_clock::to_time_t(next_hour2);
std::cout << "tomorrow time stamp is " << time_t_next_hour2 << std::endl;
std::cout << "tomorrow time is " << ctime(&time_t_next_hour2) << std::endl;

return 0;
}

运行结果如下:

1
2
3
4
5
6
7
8
now time stamp is 1586662332
now time is Sun Apr 12 11:32:12 2020

tomorrow time stamp is 1586748732
tomorrow time is Mon Apr 13 11:32:12 2020

tomorrow time stamp is 1586665932
tomorrow time is Sun Apr 12 12:32:12 2020

steady_clock

这是一个单调时钟,一旦启动之后就与系统时间没有关系了,完全根据物理是时间向前移动,成员函数只有一个 now(),通常可以用来计时,使用方法与 system_clock 相比简单许多,下面写个小例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <chrono>
#include <iostream>
int main()
{
// 先记录程序运行时间
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();

volatile int nDstVal, nSrcVal;
for (int i = 0; i < 1000000000; ++i)
nDstVal = nSrcVal;

// 做差值计算耗时
std::chrono::duration<double> duration_cost = std::chrono::duration_cast<
std::chrono::duration<double> >(std::chrono::steady_clock::now() - start);
std::cout << "total cost " << duration_cost.count() << " seconds." << std::endl;

return 0;
}

运行结果如下:

1
total cost 1.9424 seconds.

time point

这个类与 duration 类似,同样是模板类,表示具体的时间点,比如今天 18:00 开饭,明天上午 10:00 发版本,今年 5 月 1 日可能因为疫情不让出去玩了,像这些具体的时间点可以使用 std::chrono::time_point 来表达,它的定义如下:

1
2
template<class Clock, class Duration = typename Clock::duration>
class time_point;

首先这个类是在 std::chrono 这个命名空间下,但是你会经常看到以下这种写法:

1
2
std::chrono::system_clock::time_point today = std::chrono::system_clock::now();
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();

好像 time_point 又在 std::chrono::system_clockstd::chrono::steady_clock 范围内,实际上这两个范围内的 time_point 引用的是 std::chrono::time point,看看 std::chrono::system_clock 的定义能明白一些。

1
2
3
4
5
6
7
8
9
10
11
12
class system_clock {
public:
using rep = /*see description*/ ;
using period = ratio</*unspecified*/, /*unspecified*/ >;
using duration = chrono::duration<rep, period>;
using time_point = chrono::time_point<system_clock>;
static constexpr bool is_steady = /*unspecified*/ ;
static time_point now() noexcept;
// Map to C API
static time_t to_time_t (const time_point& t) noexcept;
static time_point from_time_t(time_t t) noexcept;
};

对照上面的定义可以知道,std::chrono::system_clock::time_point 实际上 std::chrono::time_point<system_clock>,这几个时间类的定义相互引用,看到这一部分的时候一定不要烦躁,一步步推导分析其中的关系。

time_point 这个类有一个成员函数 time_since_epoch() 用来获得 1970-01-01 00:00:00time_point 时间经过的 duration, 返回的 duration 的单位取决于 timepoint 定义时的 duraion 的单位,不过你也可以得到 duration 之后使用 std::chrono::duration_cast<T>() 函数来转化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <chrono>
#include <iostream>
int main()
{
// 获得epoch 和 now 的时间点
std::chrono::time_point<std::chrono::system_clock> epoch =
std::chrono::time_point<std::chrono::system_clock>{};
std::chrono::time_point<std::chrono::system_clock> now =
std::chrono::system_clock::now();

// 显示时间点对应的日期和时间
time_t epoch_time = std::chrono::system_clock::to_time_t(epoch);
std::cout << "epoch: " << std::ctime(&epoch_time);
time_t today_time = std::chrono::system_clock::to_time_t(now);
std::cout << "today: " << std::ctime(&today_time);

// 显示duration的值
std::cout << "seconds since epoch: "
<< std::chrono::duration_cast<std::chrono::seconds>(epoch.time_since_epoch()).count()
<< std::endl;

std::cout << "today, ticks since epoch: "
<< now.time_since_epoch().count()
<< std::endl;

std::cout << "today, hours since epoch: "
<< std::chrono::duration_cast<std::chrono::hours>(now.time_since_epoch()).count()
<< std::endl;

return 0;
}

运行结果如下:

1
2
3
4
5
epoch: Thu Jan  1 08:00:00 1970
today: Sun Apr 12 12:30:04 2020
seconds since epoch: 0
today, ticks since epoch: 1586665804624992500
today, hours since epoch: 440740

从运行结果来看,epoch 的时间点是 Thu Jan 1 08:00:00 1970,为什么不是 1970-01-01 00:00:00 呢?那是因为我们在东8区,格林威治时间为
1970-01-01 00:00:00 的时候,我们的时间就是 Thu Jan 1 08:00:00 1970,这样看来 std::ctime() 这个函数考虑了时区的影响,相同的代码如果在韩国同时运行得到的可能就是 epoch: Thu Jan 1 09:00:00 1970

关于时间的思考

思考一个问题,时间是不是一种不变的量,或者换一种说法,它是不是一种均匀的量。如果了解过《三体》中的部分章节,你就会发现时间总在被任意改变着。但是在现实生活中好像时间就是一个标准,我们认为它是一成不变的,总是感觉今天的1天和昨天的24小时在时间上是等同的,今年的这一年和去年的365天是等同的,但其实你了解一下闰年、闰秒、夏令时就会发现,前面提到的这些未必等同。

日常生活中对时间的描述只是为了理解和阐明一些事物,我们把太阳升到头顶叫做中午,把地球自转一圈叫做一天24小时,把地球围绕太阳公转一圈叫做1年365天,但是地球自转不是那么均匀的,也就是说每转一圈占用的绝对时间是不一样的,我们现在使用的时钟通常是滴答滴答一秒秒的走着,如果地球自转一圈的时间不是完全相同的,那么建立在这个滴答上的一切时间都是不准确的。

什么是建立在滴答滴答上的时间,我们以滴答一次作为1秒来计算,那么1分钟是60秒,也就是滴答60次,1小时是60分钟,滴答3600次,一天是24小时,滴答86400次,滴答的次数是均匀的,但是自转和公转是不均匀的,那么两个时间就对不上了,所以出现了闰秒、闰年等方法来调整时间,使得我们用来描述生活的时间和周围的环境现象可以一致,不然大约几千年以后就会出现中午12点天上出现月亮的奇观,那时的人们在史书中会发现我们这个时代中午12点挂在天上的是太阳,简直太玄幻。

有没有一种计时可以描述这种不均匀的自转呢?其实我们伟大的古人早已经发明出来了,你一定听说过日晷这种计时工具,它是观测日影记时的仪器,主要是根据日影的在日晷面上的位置,以指定当时的时辰或刻数,是我国古代较为普遍使用的计时仪器。为什么它没有时间不一致的问题?因为它本身就是不均匀的,它是根据自然现象来规定生活中每天的时间的,其实对照现在来说就是每个时辰的滴答数实际上是不一样的。

日晷这种不均匀的计时其实是为了适应天文现象,方便人们的生产生活,所以说现在地球自转一圈是一天,但不一定是86400秒,地球公转一圈是一年,但不一定是365天,后来人们使用电子设备计时,按道理来说应该非常准确,但是因为地球自转、公转的速率都不稳定,这种差距渐渐地会给生活带来困扰,于是又发明了一个折中的协调世界时,会在适当的时候闰秒、闰天,以弥补这种差距。假如你买了一个绝对精准的不联网的电子计时器,但是几年之后你就会发现你的计时器肯定和大家使用的标准时间不一致了。

其实还有一种基于特定铯原子的振荡周期来确定的国际原子时,主要是在时间精度要求较高的航天、通讯、电子等领域,为了保持系统的连续性而使用的,在日常生活中基本不会使用,但是这个时间是相对恒定的,不会去计较天文现象,每一秒都“准确”的流逝着。

时间函数思考

现在回过头来再来看这些时间函数,是不是感觉有点不一样了,比如 time(NULL) 这个函数,它返回的是从 1970-01-01 00:00:00 到现在时间的秒数,回忆一下上面关于时间的思考,这个秒数真的是准确的吗?其实你如果理解了上面的内容就能得出结论,它肯定和国际原子时是有出入的。

再考虑下闰秒的影响,假如你实现了一个函数,第一次执行是在0点执行,执行之后你设置了一个86400秒的倒计时,也就是1天的时间,到第二天0点的时候正好又执行,你又设置了一个86400秒的倒计时,但今天正好是闰秒的日子,也就是今天会比昨天多1秒,那么今天的时间到23:59:59的时候就经过了86400秒,也就是说在23:59:59的时候就会执行你写的函数,如果碰到秒杀就尴尬了…

一般的程序开发不用太考虑闰秒的影响,但是如果这一秒的误差出现的宇宙飞船的飞行中,可能会导致几十公里的误差,所以程序员们一定要理解闰秒的可能带来的问题,评估自己所写的代码需不需要处理这种情况。曾经的一次闰秒直接导致了芬兰航空系统的瘫痪,所以一些大型项目还是会提前很长时间就把即将到来的闰秒处理写入到自己的系统中,以应对它带来的危险。

当你认为时间不会倒流的时候,它确实就发生了。我们一般假定时间不会倒流,但是如果你过分依赖这个特性,可能就会导致一些问题,这种情况常常出现设定了自动校准时间的电脑上,电脑的时间走快了,然后到达一定的差距后会触发校准程序,这时就会出现“时间倒流”的现象,比如 time(NULL) 这种依赖于电脑时间的函数,在这种情况下函数返回值就会变小,出现不单调性。

总结

  • 关于时间的操作真的太多了,我居然发现一种名为 operator""h 的操作符,与数字连用表示小时,有兴趣的话可以自己扩展学习一下。
  • durationsclockstime points 三种有关时间操作的定义相互之间是有引用的,需要理清其中的关系。
  • 需要了解闰秒、闰年、天文时、原子时、协调时产生的原因,这样就可以做到熟悉原理,心里不慌。
  • 在测试的例子中出现了时区的概念,其实是人们为了生产生活主动创造出来以适应自然现象的。
  • 这里抛出一个疑问,我之前刚接触时晕乎了很久,后来渐渐才明白,有些时间函数的说明中会提到与时区无关,比如 time(NULL)、还有今天学习的 system_clock,但是当我修改电脑时区的时候会发现,这些函数的返回值会发生突变,大家有探究过其中的原因吗?

我们都是追逐时间奔跑的蝼蚁,改变世界的同时也被时间改变着。

Albert Shi wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客