c++ 标准库中chrono库的运用
C++标准库中的chrono库提供了时间计量的支持,包括用于测量时间段的duration类、用于表示时间点的time_point类,以及用于跟踪时间的时钟(clock)类。chrono库提供了高精度的时钟,允许用户测量程序执行的时间,或者实现精确的时间戳。同时,chrono库还提供了一些辅助函数,帮助我们完成时间计算、格式化时间的操作。总之,chrono库是一个非常实用的工具库,可以帮助我们方便地处理时间相关的问题。要学习chrono,就得从分数开始,分数是chrono库的基石,让我们开始吧。
std::ratio<> 编译期的分数运算,并且把结果降至最简式
ratio 定义
typedef long long intmax_t; // 位于stdint.h中
namespace std{
template <intmax_t N, intmax_t D = 1>
struct ratio {
static constexpr intmax_t num; // 分子
static constexpr intmax_t den; // 分母 denominator
using type = ratio<num, den>;
};
}
ration 小demo
#include <chrono>
#include <stdio.h>
int main()
{
std::ratio<3, 5> three_five;
printf("%lld/%lld\n", three_five.num, three_five.den);
std::ratio<30, 50> tf;
printf("%lld/%lld\n", tf.num, tf.den);
std::ratio<5, -3> five_three;
printf("%lld/%lld\n", five_three.num, five_three.den);
std::ratio<-50, 30> ft;
printf("%lld/%lld\n", ft.num, ft.den);
int a = 0;
int b = 1;
//std::ratio<a, b> a_b; 编译错误,因为a, b 不能做编译期运算
}
输出
3/5
3/5
5/3
5/3
ratio 类型四则运算以及大小比较(编译期进行)
std::ratio_add 求和 结果为std::ratio<>
std::ratio_subtract 求差 结果为std::ratio<>
std::ratio_multiply 乘法求积 结果为std::ratio<>
std::ratio_divide 除法求商 结果为std::ratio<>
std::ratio_equal 相等 结果为true_type 或者 false_type
std::ratio_not_equal 不相等 结果为true_type 或者 false_type
std::ratio_less 小于 结果为true_type 或者 false_type
std::ratio_less_equal 小于等于 结果为true_type 或者 false_type
std::ratio_greater 大于 结果为true_type 或者 false_type
std::ratio_greater_equal 大于等于 结果为true_type 或者 false_type
标准库定义的一些ratio
using atto = ratio<1, 1000000000000000000LL>;
using femto = ratio<1, 1000000000000000LL>;
using pico = ratio<1, 1000000000000LL>;
using nano = ratio<1, 1000000000>;
using micro = ratio<1, 1000000>;
using milli = ratio<1, 1000>;
using centi = ratio<1, 100>;
using deci = ratio<1, 10>;
using deca = ratio<10, 1>;
using hecto = ratio<100, 1>;
using kilo = ratio<1000, 1>;
using mega = ratio<1000000, 1>;
using giga = ratio<1000000000, 1>;
using tera = ratio<1000000000000LL, 1>;
using peta = ratio<1000000000000000LL, 1>;
using exa = ratio<1000000000000000000LL, 1>;
chrono 库一览
duration 时间段
tick 片刻数
timepoint 时间点
epoch 起始点
clock 时钟
时间段duration 说白了就是一个时间单位的片刻数,我们以秒为单位,那么一分钟是60秒,那么时间段也就是60秒,这里的60就是片刻数。
时间点timepoint 就是始点epoch和时间段duration的组合,比如起始点为中午12点,经过时间段2个小时,那么就是下午2点钟了,比12点早两个小时,那么就是早上12点了。
clock 时钟是个对象,它定义了timepoint的起始点和一个tick周期,不同的时钟有不同的起始点,比如system_clock 起始点为1970年1月1日,tick 周期可能是100纳秒,看实现咯
他们关联具体看下图:
duration(时间段)
- 定义
duration 是tick 和一个分数ratio(单位时间秒)的组合,如:
其中ratio<1>是ratio<1, 1> 也就是1/1 代表的是秒,_Rep代表是有多少个片刻,也就是多少个秒。如:template <class _Rep, class _Period = ratio<1>> class duration;
// duration<60> 等于 duration<60, ratio<1,1>> 60个一秒也就是代表一分钟,意味着这个时间段是一分钟。 std::chrono::duration<60> oneMinute; std::chrono::duration<60> sixtysecond;
- 我们试着使用duration 来表示时 分 秒
输出#include <chrono> #include <stdio.h> #include <iostream> // 定义我们自己的时间输出函数 template <typename V, typename R> std::ostream &operator << (std::ostream &s, const std::chrono::duration<V, R> &d) { s << "tick:" << d.count() << " period:" << R::num << "/" << R::den ; return s; } int main() { // 1000ns = 1μs // 1000μs = 1ms // 1000ms = 1s // 60s = 1minute std::chrono::duration<int, std::ratio<1, 1>> s(1); // 1秒钟 std::cout << "s\t" << s << std::endl; std::chrono::duration<int, std::ratio<1, 1000>> ms(1); // 1毫秒 std::cout << "ms\t" << ms << std::endl; std::chrono::duration<int, std::ratio<1, 1000000>> us(1);// 1微妙 std::cout << "us\t" << us << std::endl; std::chrono::duration<int, std::ratio<1, 1000000000>> ns(1); // 1纳秒 std::cout << "ns\t" << ns << std::endl; std::chrono::duration<int, std::ratio<60, 1>> minutes(1); // 1分钟 std::cout << "minutes\t" << minutes << std::endl; std::chrono::duration<int, std::ratio<60 * 60, 1>> hour(1); // 1小时 std::cout << "hour\t" << hour << std::endl; std::chrono::duration<int, std::ratio<60 * 60 * 24, 1>> day(1); // 1天 std::cout << "day\t" << day << std::endl; std::chrono::duration<int, std::ratio<60 * 60 * 24 * 30, 1>> mouth(1); // 1个月 std::cout << "mouth\t" << mouth << std::endl; }
s tick:1 period:1/1 ms tick:1 period:1/1000 us tick:1 period:1/1000000 ns tick:1 period:1/1000000000 minutes tick:1 period:60/1 hour tick:1 period:3600/1 day tick:1 period:86400/1 mouth tick:1 period:2592000/1
- c++ 标准库定义了常用的时分秒 微秒 纳秒等数据
namespace std { namespace chrono { using nanoseconds = duration<long long, nano>; using microseconds = duration<long long, micro>; using milliseconds = duration<long long, milli>; using seconds = duration<long long>; // = duration<long long, ratio<1,1>> using minutes = duration<int, ratio<60>>; using hours = duration<int, ratio<3600>>; #if _HAS_CXX20 using days = duration<int, ratio_multiply<ratio<24>, hours::period>>; using weeks = duration<int, ratio_multiply<ratio<7>, days::period>>; using years = duration<int, ratio_multiply<ratio<146097, 400>, days::period>>; using months = duration<int, ratio_divide<years::period, ratio<12>>>; #endif // _HAS_CXX20 } }
- duration 构造以及公开成员及成员方法
一些时间的构造以及隐式转换举例duration d 构造空的duration duration d2(d) 拷贝构造 d可能拥有不同period duration d(value) 使用d这种时间类型构建一个tick 为value 的duration d = d2 将d2 赋值给d 这里可能存在隐士转换 d.count() 返回d的片刻数 duration_cast<D>(d) 将d强转为D类型 duration::zero() 获取长度为0的duration duration::max() 返回这种类型最大的duration duration::mmin() 返回这种类型最小的duration duration::rep rep 的类型 duration::period period的类型,类型是ratio
#include <chrono> #include <stdio.h> #include <iostream> // 定义我们自己的时间输出函数 template <typename V, typename R> std::ostream &operator << (std::ostream &s, const std::chrono::duration<V, R> &d) { s << "tick:" << d.count() << " period:" << R::num << "/" << R::den ; return s; } int main() { // 如果还是不能理解std::chrono:: seconds、minutes 等的表示,可以直接把tick 理解为秒钟在转换对应的时分秒 std::chrono::duration<int> d(20); std::cout << d << std::endl; std::chrono::seconds sec(60); std::cout << sec << std::endl; // std::chrono::minutes min = sec; 不能隐式转换为分钟,因为有精度丢失 // 使用duration_cast 强转 std::chrono::minutes min = std::chrono::duration_cast<std::chrono::minutes>(sec); std::cout << min << std::endl; // 不足一分钟强转 sec = std::chrono::seconds(20); // 此处存在精度损失 不足一分钟是0分钟 std::cout << std::chrono::duration_cast<std::chrono::minutes>(sec) << std::endl; // 可以把大单位转为小单位 例如可把分钟隐式转为秒钟 std::chrono::seconds s = min; std::cout << s << std::endl; std::cout << std::chrono::seconds(std::chrono::minutes(3)); }
- duration 的计算
duration 的加减乘除都是返回duration ,如果两个duration 不同,会自动进行自动转换之后在相加减,比如小时和分钟相加,最后是得到分钟的duration,反之则不行,因为分钟不能隐式转换为小时
d1 + d2 返回相加之后的duration
d1 - d2 返回相减后的duration
d1 / d2 返回rep 类型 相除的duration,代表两个duration差多少倍,
d1 % d2 两个duration取余
d % value 返回duration,对某个值取余
d * value tick * 一个数据,也就是duration::count() 得到的数扩大value倍
value * d 也就是duration::count() 得到的数扩大value倍
d / value duration::count() 得到的数缩小value倍
d1 == d2 返回bool,两个duration相等
d1 != d2 返回bool,两个duration不等
d1 < d2 返回bool,小于
d1 <= d2 返回bool,小于等于
d1 > d2 返回bool,大于
d1 >= d2 返回bool,大于等于
++d tick++ 返回duration, 先加后运算
d++ tick++ 返回duration,先运算再加
–d tick-- 返回duration,先减后运算
d-- tick-- 返回duration,先运算后减
d += d1 返回duration,同加法
d -= d1 返回duration, 同减法
d *= value 返回duration,同乘一个数
d /= value 返回duration,同除一个数
d %= value 返回duration,同取余运算
d %= d1 同返回duration,取余运算\
输出std::chrono::minutes d1(60); std::chrono::seconds d2(60); std::chrono::seconds d(20); std::cout << "d1 + d2 \t" << d1 + d2 << std::endl; std::cout << "d1 - d2 \t" << d1 - d2 << std::endl; std::cout << "d1 / d2 \t" << d1 / d2 << std::endl; std::cout << "d2 / d1 \t" << d2 / d1 << std::endl; // 小除以大,进度损失为0 std::cout << "d1 % d2 \t" << d1 % d2 << std::endl; std::cout << "d * 2 \t" <<d * 2 << std::endl; std::cout << "2 * d \t" << 2 * d<< std::endl; std::cout << "d1 % d2 \t" << d1 % d2 << std::endl; std::cout << "d / 2 \t" << d / 2 << std::endl; std::cout << "d1 == d2 \t" << (d1 == d2) << std::endl; std::cout << "d1 != d2 \t" << (d1 != d2) << std::endl; std::cout << "++d \t" << ++d << std::endl; std::cout << "d++ \t" << d++ << std::endl; auto d3 = d + d1; std::cout << "d3 \t" << d3 << std::endl; d3 += d; std::cout << "d3 \t" << d3 << std::endl;
这里的取余运算,我们可以把小单位转换成不同的单位,并且保留精度,比如我们%minutes 可以得到秒钟余数, 我们取余second 可得毫秒余数d1 + d2 tick:3660 period:1/1 d1 - d2 tick:3540 period:1/1 d1 / d2 60 d2 / d1 0 d1 % d2 tick:0 period:1/1 d * 2 tick:40 period:1/1 2 * d tick:40 period:1/1 d1 % d2 tick:0 period:1/1 d / 2 tick:10 period:1/1 d1 == d2 0 d1 != d2 1 ++d tick:21 period:1/1 d++ tick:21 period:1/1 d3 tick:3622 period:1/1 d3 tick:3644 period:1/1
输出#include <chrono> #include <stdio.h> #include <iostream> // 定义我们自己的时间输出函数 template <typename V, typename R> std::ostream &operator << (std::ostream &s, const std::chrono::duration<V, R> &d) { s << "tick:" << d.count() << " period:" << R::num << "/" << R::den ; return s; } int main() { // 定义20ms * 30s * 3minute std::chrono::milliseconds ms(20 + 30 * 1000 + 3 * 60 * 1000); // 分钟直接转换 auto mm = std::chrono::duration_cast<std::chrono::minutes>(ms); // 取余分钟可得秒钟余数 auto second = std::chrono::duration_cast<std::chrono::seconds>(ms % std::chrono::minutes(1)); std::cout << "total ms=" << ms << std::endl; std::cout << "minutes=" << mm << std::endl << "second=" << second << std::endl << // 取余秒钟数可得毫秒余数 "millisecond=" << ms % std::chrono::seconds(1) << std::endl; }
total ms=tick:210020 period:1/1000 minutes=tick:3 period:60/1 second=tick:30 period:1/1 millisecond=tick:20 period:1/1000
clock(时钟)和时间点(timepoint)
clock 定义一个起始点epoch,和tick周期,比如起始点为程序的开始,tick为纳秒的时钟
timepoint 是clock 是起始点向前或者向后的一段duration。
两者的关联
clock
clock::duration 获取clock的duraion类型
clock::rep 获取tick的类型,等价于clock::duration::rep
clock::period 获取period类型,等价于clock::ruration::period
clock::time_point 获取clock的timepoint 类型
clock::now 静态方法,得到一个表示现在时间点的tiem_point
timepoint
time_point::clock 关联的时钟类型
time_point::duration 关联的时钟duration
time_point::time_since_epoch 表示clock 的起始点到现在时间点经过的duration
timepoint 的定义
namespace std
{
namespace chrono
{
template <class _Clock, class _Duration = typename _Clock::duration>
class time_point
{
using clock = _Clock; // 时钟类型
using duration = _Duration; // 时间段类型
using rep = typename _Duration::rep;
using period = typename _Duration::period;
time_point(); // 默认构造
time_point(const _Duration& _Other); // 拷贝构造
ime_point(const time_point<_Clock, _Duration2>& _Tp); // 用clock 的起始点构造duration 之后的时间点
_Duration time_since_epoch(); // 起始点到现在时间点经过的duration
...
}
}
}
timepoint 的各种运算
//1. 构造
timepoint t 使用clock 提供的epoch 构造一个timepoint
timepoint t(tp1) 拷贝构造
timepoint t(d) 使用clock 提供的epoch 构造一个duration 后timepoint
time_point_cast<C, D>(t) 时间点的转换,可以抓换为不同时钟和不同的duration
// 计算 只有加减运算,且只能和duration 和timpoint 计算
t += d 返回新的timepoint,值为t + duration
t -= d 返回新的timepoint,值为t - duration
t + d 返回新的timepoint,值为t + duration
d + t 返回新的timepoint,值为t + duration
t - d 返回新的timepoint,值为t - duration
t1 - t2 返回duration 值为两个时间点之间的duration
// 比较 只能是时间点与时间点比较
t1 == t2 返回bool
t1 != t2 返回bool
t1 < t2 返回bool
t1 <= t2 返回bool
t1 > t2 返回bool
t1 >= t2 返回bool
// public成员
t.time_since_epoch() 返回duration,值为epoch 和timepoint 之间的时间段
timepoint::min() 返回timepoint, 得到最小的时间点
timepoint::max() 返回timepoint, 返回最大的时间点
timepoint 示例代码
/ 定义我们自己的时间输出函数
template <typename V, typename R>
std::ostream &operator << (std::ostream &s, const std::chrono::duration<V, R> &d)
{
s << "tick:" << d.count() << " period:" << R::num << "/" << R::den ;
return s;
}
int main()
{
// 使用精准时钟
using timepoint = std::chrono::time_point<std::chrono::steady_clock>;
timepoint t;
std::cout << "t info" << t.time_since_epoch() << std::endl;
timepoint t1(timepoint::duration(5000));
std::cout << "t1 info" << t1.time_since_epoch() << std::endl;
t += timepoint::duration(2000);
std::cout << "t info" << t.time_since_epoch() << std::endl;
auto t2 = t + timepoint::duration(2000);
std::cout << "t2 info" << t2.time_since_epoch() << std::endl;
// 比较
std::cout << "t1 == t2 \t" << (t1 == t2) << std::endl;
std::cout << "t1 != t2 \t" << (t1 != t2) << std::endl;
std::cout << "t1 < t2 \t" << (t1 < t2) << std::endl;
std::cout << "t1 <= t2 \t" << (t1 <= t2) << std::endl;
std::cout << "t1 > t2 \t" << (t1 > t2) << std::endl;
std::cout << "t1 >= t2 \t" << (t1 >= t2) << std::endl;
// 最大最小
std::cout << "min info" << timepoint::min().time_since_epoch() << std::endl;
std::cout << "mac info" << timepoint::max().time_since_epoch() << std::endl;
return 0;
}
输出
t infotick:0 period:1/1000000000
t1 infotick:5000 period:1/1000000000
t infotick:2000 period:1/1000000000
t2 infotick:4000 period:1/1000000000
t1 == t2 0
t1 != t2 1
t1 < t2 0
t1 <= t2 0
t1 > t2 1
t1 >= t2 1
min infotick:-9223372036854775808 period:1/1000000000
mac infotick:9223372036854775807 period:1/1000000000
标准库的三个时钟
- std::chrono::system_clock 系统时钟,将timepoint关联到系统时钟上,他是不稳定的时钟,如果用户修改了系统时间,那么它会被修改
namespace std { namespace chrono { struct system_clock { using rep = long long; using period = ratio<1, 10'000'000>; // 100 nanoseconds using duration = duration<rep, period>; using time_point = time_point<system_clock>; static constexpr bool is_steady = false; // 不稳定时钟 static time_point now(); // 当前时间 static __time64_t to_time_t(const time_point& _Time); // 把时间点转为数值,转为std::time_t static time_point from_time_t(__time64_t _Tm); // 把一个数值转为时间点 std::time_t 转为timepoint }; } }
- std::chrono::steady_clock 稳定时钟,他能保证时间稳定的流逝,不会被修改,可能是程序启动的时间,也有可能是系统启动的时间
namespace std { namespace chrono { struct steady_clock { // wraps QueryPerformanceCounter using rep = long long; using period = nano; using duration = nanoseconds; using time_point = time_point<steady_clock>; static constexpr bool is_steady = true; // 是稳定时钟 static time_point now(); // 返回现在的时间点 }; } }
- std::chrono::high_resolution_clock 表示系统中拥有最短tick 周期的clock 一般是纳秒,也不排除有些系统是毫秒的。
namespace std { namespace chrono { // 可能的实现 哈哈 using high_resolution_clock = steady_clock; } }
标准库中data/time相关的函数
std::clock_t 数值类型,用来表示cpu 消逝时间,由std::clock()返回
std::time_t 数值类型, 表示timepoint
struct tm 结构体, 包含时分秒 年月日 星期 一年中的第几天
std::clock() 获取cpu 消耗时间
std::time() 返回数值类型,获取当前时间
std::difftime() 两个time_t之间的插,double类型,单位秒
std::localtime() 使用本地时区把time_t 转为一个tm
std::gmtime() 不考虑时区,把time_t 转为tm
std::asctime() 把tm 转为标准日志格式化字符串
std::strftime() 把tm 转化为用户自定义的日历时间格式化字符串
std::ctime() 转换time_t 为标准日历时间,并且考虑时区,等于 asctime(localtime(time_t))
std::mktime() 转换tm 为time_t
#include <chrono>
#include <stdio.h>
#include <iostream>
#include <ctime>
// 定义我们自己的时间输出函数
template <typename V, typename R>
std::ostream &operator << (std::ostream &s, const std::chrono::duration<V, R> &d)
{
s << "tick:" << d.count() << " period:" << R::num << "/" << R::den ;
return s;
}
int main()
{
// 时间点
auto t = std::chrono::system_clock::now();
std::cout << "now " << t.time_since_epoch() << std::endl;
// 把时间点转为time_t
std::time_t st = std::chrono::system_clock::to_time_t(t);
// 把time_t 转为std::tm
struct std::tm *stm = std::localtime(&st);
if (stm)
{
printf("%d-%d-%d %d:%d:%d\n",
stm->tm_year, stm->tm_mon, stm->tm_mday, stm->tm_hour, stm->tm_min, stm->tm_sec);
}
// 把tm 转为time_t
std::time_t st1 = std::mktime(stm);
// 把time_t 转为时间点
auto tp = std::chrono::system_clock::from_time_t(st1);
// 打印时钟
std::cout << "now " << tp.time_since_epoch() << std::endl;
return 0;
}
输出
now tick:16824202401983799 period:1/10000000
123-3-25 18:57:20
now tick:16824202400000000 period:1/10000000
可以看出tick 不一样了,这是因为啊,time_t 时间单位是秒,相当于秒后面的数据就被抹杀了,转换有精度损失,可以看下from_time_t的实现
使用了t(d) 使用了duration 构造,本来是100纳秒,转为秒,肯定是有精度损失的。
time_point{seconds{_Tm}};
综合运用
假如我们要开发一个渲染软件,那么肯定会和帧率挂钩了,就是每秒钟我们需要稳定的输出多少张图片,或者是在屏幕上绘制多少张画面,比如33张,那么我们就可以说fps为33
那我们如和在一个循环中保证每一秒钟运行33次呢,答案就是让线程睡眠cpu用不掉的时机,比如fps是33,那么一秒钟运行33次,运行的时间是30毫秒多点, 那假设cpu才耗费20毫秒,
还有十毫秒,那我们就得睡眠十毫秒,但是问题就在这里,cpu并不是每次都消耗20毫秒,有可能一次消耗10 ,一次消耗15,一次消耗20是,那么我们就需要一个期望时间,
期望时间 = currentTimepoint + duration(30ms);
睡眠时间 = 期望时间 - 运行耗时
具体看demo
#include <iostream>
#include <chrono>
#include <thread>
int main()
{
const int FRAMES_PER_SECOND = 33;
const int SKIP_TICKS = 1000 / FRAMES_PER_SECOND;
std::chrono::steady_clock::time_point now, schedule;
std::chrono::steady_clock::time_point fpsTime;
int fps = 0;
auto skip_time = std::chrono::milliseconds(SKIP_TICKS);
auto deviation = std::chrono::milliseconds(3);
auto oneSecond = std::chrono::milliseconds(1000);
while (true)
{
now = std::chrono::steady_clock::now();
schedule = now + skip_time;
{
// 做一些事情, 这里使用睡眠代替耗时
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
// 此处now 是为了计算fps 是否超时
now = std::chrono::steady_clock::now();
// 这里只是为了打印fps ,具体业务可去掉
if (now - fpsTime >= oneSecond)
{
printf("----------------fps:%d\n", fps);
fpsTime = now;
fps = 0;
}
fps++;
now = std::chrono::steady_clock::now();
// 计算应该睡眠几秒
auto diff = schedule - now;
// 睡眠修正后的时间
if (diff >= deviation)
{
std::this_thread::sleep_for(diff);
}
}
return 0;
}
输出
----------------fps:0
----------------fps:30
----------------fps:31
----------------fps:31
----------------fps:31
----------------fps:31
----------------fps:31
----------------fps:30
----------------fps:31
----------------fps:31
----------------fps:31
----------------fps:31
----------------fps:31