原文链接: Clock和Timer(chrono库)
一、Clock和Timer
- 在过去,C和POSIX提供的系统时间接口,允许从秒转换至毫秒,再至微秒,最终至纳秒,问题是每次转换就需要一个新接口
- 基于这个原因,C++11开始提供一个精度中立的程序库,称为chrono程序库,被定义于<chrono>中
二、Chrono程序库概述
- Chrono程序库的设计,是希望能够处理“timer和clock在不同系统中可能不同”的事实,同时也是为了强化实践精准度
- 为了避免像POSIX的time程序库那样每十年就引入一个新的时间类型,C++标准库的目标是提供一个精度中立概念,把duration(时间段)和timepoint(时间点)从特定clock(时钟)区分开来。最终结果就是chrono程序库核心由以下类型或概念组成:
- duration(时间段):值得是在某时间单位上的一个明确的tick(片刻数)。例如,“3分钟”就是指一个时间段
- timepoint(时间点):一个duration和一个epoch(起始点)的组合。例如,“2000年新年夜”就是一个时间点
- Chrono程序库定义在<chrono>头文件中,命名空间为namespace std::chrono
三、Duration(时间段)
duration的基本格式
- Duration是一个数值和一个分数的组合(其中的分数由ratio<>描述)
- 模板参数1:是一个数值,用来表示时间段所经历的滴答(tick)数
- 模板参数2:用来表示时间段每个滴答的单位(秒、毫秒等等)。可以省略,省略时默认以秒为单位
- 例如:
- twentySeconds:代表20秒,20*1秒=20秒(以秒为单位)
- twentySeconds:代表0.5分钟,0.5*60秒=0.5分钟(以分钟为单位(60/1))
- twentySeconds:代表1毫秒,1*0.001秒=1毫秒(以毫秒为单位(1/1000))
std::chrono::duration<int> twentySeconds(20);
std::chrono::duration<double, std::ratio<60>> halfAMinute(0.5);
std::chrono::duration<long, std::ratio<1, 1000>> oneMillisecond(1);
chrono自带的duration类型定义
- 在chrono中自定义了下面的类型,可以直接使用
- 例如,下面可以轻松的指定一些时间段:
std::chrono::seconds twentySeconds(20); //20秒
std::chrono::hours aDay(24); //24小时
std::chrono::milliseconds oneMillisecond(1); //1毫秒
Duration的算术运算
下图列出了duration可以进行的算术运算。例如:
- 你可以计算两个duration的和、差、积和商
- 你可以加减tick,或加减其他duration
- 你可以比较两个duration的大小
运算所涉及的两个duration的单位类型可以不同:
- 标准库的common_type<>为duration提供了一个重载版本
- 因此运算所得的那个duration,其单位将是两个操作数的单位的最大公约数演示案例:
std::chrono::seconds d1(42); //42秒
std::chrono::milliseconds d2(10); //10毫秒
d1 - d2; //返回41990个毫秒为单位的一个duration,42000-10=41990
d1 < d2; //返回false
std::chrono::duration<int, std::ratio<1, 3>> d1(1); //1/3秒
std::chrono::duration<int, std::ratio<1, 5>> d2(1); //1/5秒
d1 + d2; //返回8/15秒,1/3+1/5=8/15
d1 < d2; //返回false
- 你也可以将duration转换为不同的单位,只要彼此之间存在隐式转换即可。因此,你可以将小时转换为秒,但是反向不可以(详情还可以见下面的duration_cast<>()的介绍)。例如:
std::chrono::seconds twentySeconds(20); //20秒
std::chrono::hours aDay(24); //24小时
std::chrono::milliseconds ms; //0毫秒
ms += twentySeconds + aDay; //86400000毫秒
--ms; //86399999毫秒
ms *= 2; //172839998毫秒
std::cout << ms.count() << " ms" << std::endl;
std::cout << std::chrono::nanoseconds(ms).count() << " ns" << std::endl;
Duration的其他操作
- 下面列出了duration支持的其它操作
- duration的默认构造函数会以默认方式初始化其数值,因此基础类型的初值是不明确的
- duration提供三个静态函数:zero()产出一个0秒duration,min()和max()分别产出一个duration所能拥有的最小值和最大值
- 例如,下面为duration对象添加一个operator<<版本
template<typename V,typename R>
std::ostream& operator<<(std::ostream& s, const std::chrono::duration<V, R>& d)
{
s << "[" << d.count() << " of " << R::num << "/" << R::den << "]";
return s;
}
int main()
{
std::chrono::milliseconds d(42);
std::cout << d << std::endl;
}
duration_cast<>
- 在上面我们介绍过duration的类型转换,可以将一个低精度的单位类型转换为一个高精度的单位类型(例如,将分钟转换为秒,将秒转换为微秒),但是不能将一个高精度的单位类型转换为一个低精度的单位类型(例如,将微秒转换为秒,将秒转换为分钟等。因为这可能会造成数据的丢失,例如将42010毫秒转换为秒,结果是42,那么原本的10毫秒就丢失了)
- 如果想要将高精度的单位类型转换为一个低精度的单位类型,那么可以使用duration_cast<>进行强制转换
- 例如:
std::chrono::seconds sec(55);
//错误的,默认不能将秒转换为分钟
std::chrono::minutes m1 = sec;
//正确的,可以使用duration_cast,将秒转换为分钟
std::chrono::minutes m2 = std::chrono::duration_cast<std::chrono::minutes>(sec);
- 将浮点数类型的duration转换为整数类型的duration也需要使用duration_cast<>。例如:
std::chrono::duration<double, std::ratio<60>> halfMin(0.5);
//错误,halfMin的tick为double类型,s1的tick默认为int类型
std::chrono::seconds s1 = halfMin;
//正确,使用duration_cast强制转换
std::chrono::seconds s2 = std::chrono::duration_cast<std::chrono::seconds>(halfMin)
- 演示案例:
- 下面代码把duration切割为不同单元,例如让一个以毫秒为单位的duration切割为相对应的小时、分钟、秒钟、毫秒
- 在下面我们将ms转换为小时hh,实际数值会被截断而四舍五入
- 幸好有%运算符,我们可以把一个duration当做其第二实参,于是写下ms%std::chrono::hours(1)轻松处理剩余的毫秒,那么毫秒又被转换为分钟
std::chrono::milliseconds ms(7255042);
std::chrono::hours hh = std::chrono::duration_cast<std::chrono::hours>(ms);
std::chrono::minutes mm = std::chrono::duration_cast<std::chrono::minutes>(ms%std::chrono::hours(1));
std::chrono::seconds ss = std::chrono::duration_cast<std::chrono::seconds>(ms%std::chrono::minutes(1));
std::chrono::milliseconds msec = std::chrono::duration_cast<std::chrono::milliseconds>(ms%std::chrono::seconds(1));
std::cout << "raw: " << hh << "::" << mm << "::" << ss << "::" << msec << std::endl;
std::cout << " " << setfill('0') << setw(2) << hh.count() << "::"
<< setw(2) << mm.count() << "::"
<< setw(2) << ss.count() << "::"
<< setw(3) << msec.count() << std::endl;
四、Clock(时钟)
- Clock(时钟):
- 定义一个epoch(起始点)和一个tick周期
- 例如,某个clock也许定义tick周期为毫秒,起始点是UNIX epoch
- 此外,clock还提供一个类型给“与此clock关联”的任何timepoint使用
- Clock提供的函数now()可以产出一个代表“现在时刻”的timepoint对象
Clock提供的操作
- 下图列出了clock提供的类型定义和static成员
三个clock
- 标准库提供了三个clock:
- system_clock:它所表现的timepoint将关联至现行系统的即时时钟
- 这个clock提供便捷函数to_time_t()和from_time_t(),允许我们在timepoint和“C的系统时间类型”timet之间转换,这意味着你可以转换至日历时间
- strady_clock:它保证绝不会被调用,因此当实际时间流逝,其timepoint值绝不会减少,而且这些timepoint相对于真实时间都有稳定的前进速率
- high_resolution_clock:它所表现的是当前系统中带有最短tick周期的clock
- system_clock:它所表现的timepoint将关联至现行系统的即时时钟
- 这三个clock都支持上面Clock提供的操作
三个clock的精度问题
- 标准库并不强制规定上述clock的精准度、epoch,“最小和最大timepoint的范围”。举个例子,你的system clock也许提供的是UNIX epoch(1970年1月1日),如果你还需要一个特定的epoch,或你关注的timepoint并非被你的clock涵盖,你就必须使用各种便捷函数查清楚
- 例如,下面的函数打印某个clock的各种属性:
template<typename C>
void printClockData()
{
std::cout << "- precision: ";
typedef typename C::period P;
if (std::ratio_less_equal<P, milli>::value){
typedef typename std::ratio_multiply<P, kilo>::type TT;
std::cout << fixed << double(TT::num) / TT::den << " milliseconds" << std::endl;
}
else {
std::cout << fixed << double(P::num) / P::den << " seconds" << std::endl;
}
std::cout << "- si_steady: " << boolalpha << C::is_steady << std::endl;
}
- 我们可以针对各种clock调用这个函数,例如:
int main()
{
std::cout << "system_clock: " << std::endl;
printClockData<std::chrono::system_clock>();
std::cout << "\nhigh_resolution_clock: " << std::endl;
printClockData<std::chrono::high_resolution_clock>();
std::cout << "\nsteady_clock: " << std::endl;
printClockData<std::chrono::steady_clock>();
}
- 结果如下图所示:
- 可以看到,system_clock和high_resolution_clock有着相同精度,100纳秒,而steady_clock的精度则是毫秒
- 还可以看到,steady_clock和high_resolution_clock不能被调整
- 还需要注意,在其他系统中情况也许完全不同,例如high_resolution_clock有可能和system_clock相同
演示案例
- 用来比较程序的两个时间点是否相同,或计算差距,system_clock扮演者重要角色。例如:
//获得当前时间,保存为system_start
auto system_start = std::chrono::system_clock::now();
//...
//再次获取当前时间,与system_start进行比较,查看时间是否超过了1分钟
if (std::chrono::system_clock::now() > system_start + std::chrono::minutes(1))
{
//...
}
- 上面的代码可能行不通,因为如果clock在中间被调整,即使程序执行超过了1分钟,if也可能返回false
- 类似的,当我们处理程序的执行时间,下面的程序有可能因中间clock被调用而打印出一个负值duration
#include <windows.h> //Sleep
int main()
{
//获得当前时间,保存为system_start
auto system_start = std::chrono::system_clock::now();
Sleep(3000); //休息3秒
//获得时间差
auto diff = std::chrono::system_clock::now() - system_start;
//强制转换为秒
auto sec = std::chrono::duration_cast<std::chrono::seconds>(diff);
//打印
std::cout << "this program runs:" << sec.count() << " seconds" << std::endl;
}
- 基于相同的理由,使用timer搭配steady_clock以外的lock,有可能一旦system_clock被调用它们的duration也随之改变
五、Timepoint(时间点)
- Timepoint(时间点):
- 表现出某个特定时间点,关联至某个clock的某个正值或负值duration
- 例如,如果duration是10天,其关联的clock epoch是“1970年1月1日”,那么这个timepoint就是1970年1月11日
- timepoint提供的能力包括:产出epoch、产出“与其clock相应”的所有timepoint中的最小值和最大值,以及timepoint的各种算术运算
- timepoint定义如下:
- timepoint分为下面四种类型:
- Epoch:由任何clock的time_point的默认构造函数产出
- Current time:由任何clock的static成员函数now()产出
- Minimum timepoint:由任何clock的time_point的static成员函数min()产出
- Maximum timepoint:由任何clock的time_point的static成员函数max()产出
演示案例1
- 下面程序将timepoint赋值给tp并转换为日历表示法,然后打印出来:
std::string asString(const std::chrono::system_clock::time_point& tp)
{
std::time_t t = std::chrono::system_clock::to_time_t(tp);
std::string ts = std::ctime(&t);
ts.resize(ts.size() - 1);
return ts;
}
int main()
{
std::chrono::system_clock::time_point tp;
std::cout << "epoch: " << asString(tp) << std::endl;
tp = std::chrono::system_clock::now();
std::cout << "now: " << asString(tp) << std::endl;
tp = std::chrono::system_clock::time_point::min();
std::cout << "min: " << asString(tp) << std::endl;
tp = std::chrono::system_clock::time_point::max();
std::cout << "max: " << asString(tp) << std::endl;
}
timepoint提供的操作
- timepoint提供的操作如下:
- time_since_epoch():
- 一般而言,timepoint对象只有一个成员,是个duration,与相应的clock的epoch有着关系
- 这个timepoint值可以通过time_since_epoch()取得
timepoint的溢出
- 虽然timepoint的接口用了ratio,这确保了duration单元的溢出会导致编译器报错,但是duration的溢出还是可能会发生
- 见下面的例子:
- 这也说明了chrono是一个duration/timepoint程序库,而不是个date/time程序库。你可以计算duration和timepoint但仍然必须把epoch、最小和最大的timepoint、闰年和闰秒纳入考虑
六、C和POSIX提供的Date/Time函数
- C和POSIX提供的Date/Time函数介绍参阅:https://blog.csdn.net/qq_41453285/article/details/102651298
- C++标准也提供了C和POSIX所提供的“处理date和time”接口。原本在<time.h>内的宏、类型、函数,现在被定义在<ctime>的namespace std内
- <ctime>所提供的操作如下图所示:
- 宏CLOCK_PER_SEC定义了clock()的单位类型(它返回的是elapsed CPU time,以1/CLOCK_PER_SEC秒计)
- time_t通常只是“始自UNIX epoch(1970年1月1日)的秒数”。然而根据C和C++标准的说法,也并不保证如此
演示案例(timepoint和日历时间的转换)
七、以计时器停滞线程
- Duration和timepoint可用于线程或程序(即主线程)的停滞(block)。停滞可以是无条件的,也可以指定最大时间段,或等待一个lock或某条件成立,或等待另一线程结束
- 提供的操作如下:
- sleep_for()和sleep_until():由this_thead提供用以停滞线程
- try_lock_for()和try_lock_until():用来在等待一个mutex时指定最大时间段
- wait_for()和wait_until():用来在等待某条件成立或等待一个future时指定最大时间段
- 所有以...for()结尾的函数都会用到一个duration,所有以...until()结束的函数都会用到一个timepoint。例如:
- this_thread::sleep_for(chrono::seconds(10)); //会停滞当前线程(不无可能是主线程)10秒钟
- this_thread::sleep_until(chrono::system_clock::now()+chrono::seconds(10)); //停滞当前线程,直到system clock 来到一个“比此刻多10秒”的timepoint
- 这些调用虽然看起来相同,其实不然:
- 所有...until()函数,你需要传递一个timepoint,而它收到时间调整的影响。如果sleep_until()之后的10秒内system clock被调整了,停滞时间也会被相应调整。例如我们把system clock回调1小时,这个程序将被停滞60分钟又10秒。又如果我们把clock调快超过10秒,timer会立刻结束
- 如果使用...for()函数如sleep_for(),你必须传一个duration,或你使用steady_clock,那么system clock的调整通常不会影响duration。然而在一个不提供steady clock的硬件上,软件平台没机会在不受到“可能被调整的system time”的影响下计算秒数,因此时间的调整也会冲击...for()函数
- 所有这些timer都不会保证绝对精准。对任何timer而言都存在一点点延迟,因为系统只是周期性地检查那个timer结束了,而timer和interrupt(中断)的处理又需要花费一些时间。因此,timer的时间长度将会是它们所指定的时间加上一小段(取决于实现的质量和当下情势)