《C++并发编程实战》笔记(四—前置篇)

std::chrono

一、概述

时间库的基本组成部分包括duration(时间段)、time_point(时间点)和clock(时钟)。

时间库所在的头文件为:

  • #include <chrono>

时间库的各种类型都定义在std::chrono命名空间中。

二、duration时长类

duration用来表示一个时间间隔,即两个时间点之间的时间差。如两个小时、三天等,都属于时长。

2.1 初始化

C++通过计时单元个数计时单元确定时长,即:

时长 = 计时单元个数 × 计时单元 时长=计时单元个数\times计时单元 时长=计时单元个数×计时单元

  • 一个计时单元定义为1s的若干倍,具体的倍数可以自行指定
  • 如果定义计时单元个数为20,计时单元为1s的 1 4 \frac{1}{4} 41倍,则表示的时长就是 20 × 1 4 × 1 s = 5 s 20\times\frac{1}{4}\times1s=5s 20×41×1s=5s

因此duration类定义为有两个参数的模板:

template<
    class Rep,
    class Period = std::ratio<1>
> class duration;
  • Rep:一个数值类型。指定计时单元个数的类型,创建duration对象时需要传入一个Rep类型的数字表示计时单元的个数r。可以使用成员函数d.count()获取duration对象中计时单元的个数r
  • Period:一个数值类型。用来确定一个计时单元的大小为 该数值类型对象表示的值 × 1 s 该数值类型对象表示的值\times1s 该数值类型对象表示的值×1s(1秒的多少倍)。
    • std::ratio是一个表示有理数的模板数值类型,根据传入的两个值模板参数,构成了一个数 N u m D e n o m \frac{Num}{Denom} DenomNum的类型
      template<
          std::intmax_t Num,
          std::intmax_t Denom = 1
      > class ratio;
      
      std::ratio<1, 2> half; // half 表示 1/2
      std::ratio<7, -21> neg_one_three // 表示 -1/3 的类型
      
      为了简化部分数的表示,C++预先定义了表示0.0010.010.110100等数字的类型:
       using std::milli = std::ratio<1, 1000> // 表示 0.001 的类型
       using std::kilo = std::ratio<1000, 1>  // 表示 1000 的类型
       ```
      
    • Period如果是std::ratio<1, 2>,表示一个数 1 2 \frac{1}{2} 21的类型,该类型的对象表示数字 1 2 \frac{1}{2} 21,那么根据Period可以确定该duration中一个计时单元就是 1 2 × 1 s \frac{1}{2}\times1s 21×1s,即计时单元为0.5s。
  • duration对象表示的时长为计时单元个数×计时单元,即: r × N u m D e n o m × 1 s r\times\frac{Num}{Denom}\times1s r×DenomNum×1s。如:
    // Rep 为 double 类型,r 为 2.5
    // Period 为 std::ratio<1, 5>,Num 为 1,Denom 为 5,表示一个分数 1/5
    // 所以 d1 就表示 2.5 × 1 / 5 × 1s = 0.5s 的时长
    std::chrono::duration<double, std::ratio<1, 5>> d1(2.5)
    
    std::chrono::duration<int, std::milli> second(1000); // 表示 1秒
    

为了简化时长的表示,C++预先定义了特例化的毫秒、秒、分钟等类型:

// 毫秒
using std::chrono::milliseconds = std::chrono::duration</* int45 */, std::milli>;
// 秒
using std::chrono::seconds = std::chrono::duration</* int35 */>;

// 表示 1000 毫秒的 duration 对象
std::chrono::milliseconds ms(1000);

// 表示 10 秒的 duration
std::chrono::seconds s(10);

2.2 时长字面值后缀

在命名空间 std::literals::chrono_literals中定义了一系列字面值后缀运算符hminsms等,用于简化时长的表示:

  • std::chrono::seconds operator""s(n):返回一个表示n秒的duration对象
  • std::chrono::milliseconds operator""ms(n):返回一个表示n毫秒的duration对象

对于以上重载的字面值后缀运算符,可以传入整数浮点数,编译器会根据需要自动选择合适的durationRep的类型,因此如果对精度有特殊要求,不应该使用这种方式。

auto two_seconds = 2s;   // 等价于 std::chrono::seconds(2)
std::chrono::seconds four_seconds(4s); // 等价于调用 std::duration 的拷贝构造

2.3 duration之间隐式类型转换

std::duration的不同类型之间可以相互隐式类型转换,由于每种类型duration的计时单元是固定的,因此不同类型的转换就是将n个以p1为计时单元的duration转换为m个以p2为计时单元的duration。实际就是计算n在新的计时单元下对应的m值为多少。

如:

  • std::chrono::duration<int, std::ratio<3, 1>> int_d(6)表示n=6个以3s为单位的duration,共18s的时长
  • std::chrono::duration<double, std::ratio<2, 1>> dou_d(int_d)表示将int_d隐式转换为以2s为单位的duration,记dou_dcount()返回的计时单位的个数为m

此时double类型m的计算过程为:
6 ∗ 3 1 ∗ 1 s = m ∗ 2 1 ∗ 1 s m = 6 ∗ 3 2 = 9.0 \begin{aligned} 6*\frac{3}{1}*1s &= m*\frac{2}{1}*1s \\ m &= \frac{6*3}{2} = 9.0 \end{aligned} 6131sm=m121s=263=9.0

即计算方法为:
m = n ∗ p 1 p 2 m=\frac{n*p1}{p2} m=p2np1

为了能够正确执行转换,需要保证转换时精度不会损失,否则编译器会报错,为了判断精度是否会损失,有以下方法:

  • m是浮点数类型时才能正常转换。因为 n ∗ p 1 p 2 \frac{n*p1}{p2} p2np1的结果可能是小数,如果m不是浮点数类型,结果转换为m时浮点数的精度会丢失。
  • m如果是非浮点数类型,需要保证 p 1 p 2 \frac{p1}{p2} p2p1为整数,这样才能保证m的结果也是整数,不会出现从浮点数类型的结果转换为m时造成精度的丢失

2.4 duration之间显式类型转换

如果确实需要将高精度的类型转换为低精度的类型,可以使用std::chrono::duration_cast执行显式类型转换,但是这可能会造成精度的丢失:

// int_three_two 表示 12s 的时长
std::chrono::duration<int, std::ratio<3, 2>> int_three_two(8);
// int_seven_five count 的计算过程为 12 / (7 / 5) = 12 * 5 / 7 = 60 / 7 = 8.571...
std::chrono::duration<int, std::ratio<7, 5>> int_seven_five =
    std::chrono::duration_cast<std::chrono::duration<int, std::ratio<7, 5>>>(
        int_three_two);
        
// 由于目标的 Rep 为 int,所以转换时会丢失小数,最终输出 8
// 所以 int_seven_five 实际表示的是 8 * 7 / 5 * 1s = 11.2s 的时长
std::cout << int_seven_five.count() << std::endl;

2.5 duration对象的运算

duration类重载了多种运算符,支持多种操作

  • 与对应Rep类型的数字n的加减乘除,表示对其计数单元的个数进行对应运算
  • 两个duration类型相加减,表示计算时长累加或相减后的新时长

2.6 输出duration对象

在C++20中,duration对象还重载了输出运算符operator<<,该运算符会输出对象的cout(),并根据Period类型的不同添加后缀字符,详细的规则可以查看文档

std::chrono::seconds cout_second(2);
std::cout << "cout_second: " << cout_second << std::endl;
// cout_second: 2s

std::chrono::milliseconds cout_mill_second(300);
std::cout << "cout_mill_second: " << cout_mill_second << std::endl;
// cout_mill_second: 300ms

std::chrono::duration<double, std::ratio<2, 1>> cout_duration(2.88);
std::cout << "cout_duration: " << cout_duration << std::endl;
// cout_duration: 2.88[2]s

三、时钟类

时钟类主要用来获取当前的时间,常见的时钟类包括:

  • std::chrono::system_clock:该时钟的时间对应于操作系统提供的时间,如果修改了操作系统的时间,那么利用该类获取的时间也会随之变化(第二次获取的时间可能会早于第一次获取的时间)
  • std::chrono::steady_clock:该时钟是稳定的,即时间一定只会向前走,不会发生第二次时间比第一次早的情况
  • std::chrono::high_resolution_clock:利用上面的某种时钟定义的高精度时钟,可以提供能够实现的最短计时单元的时钟
    时钟类内部定义了一个std::chrono::duration的特例化类型,用来表示时钟的计时单元,一般由编译器确定具体的数值,如在MSVC某版本中定义为了duration<long long, ratio<1, 10'000'000>。因此,对于high_resolution_clock,其计时单元应该比其余两种更短,以提供更高的精度。

时钟类中都包含一个静态成员函数now,且其中定义了一个std::chrono::time_point的特例化类型,作为now的返回值类型:

// std::chrono::system_clock 返回的 time_point 第一个模板参数为 std::chrono::system_clock
std::chrono::time_point<std::chrono::system_clock> now_time 
                                = std::chrono::system_clock::now();

// std::chrono::steady_clock 返回的 time_point 第一个模板参数为 std::chrono::steady_clock
std::chrono::time_point<std::chrono::steady_clock> now_time 
                                = std::chrono::steady_clock::now();

四、时间点

4.1 ctime中时间的表示

C++的#include <ctime>头文件中保留了C语言中时间相关的函数

函数time用来获取当前的时间:

std::time_t time(nullptr);
  • 返回值是一个整数类型std::time_t(表示1970年到当前时间的秒数)

可以使用std::ctime函数将std::time_t类型表示的时间转换为字符串形式:

std::time_t cur_time = std::time(nullptr);
std::cout << "time_t: " << cur_time << std::endl;
// time_t: 1722080852

// ctime 将 time_t 类型的时间转换为字符串
char *cur_time_str = std::ctime(&cur_time);
std::cout << "ctime: " << cur_time_str << std::endl;
// ctime: Sat Jul 27 19:47:32 2024

为了更好的表示和处理时间,还定义了一个std::tm类型的结构体,其中包含了表示年月日,时分秒等数据成员。可以使用如下两个方法,将time_t类型表示的时间转换为std::tm类型:

  • std::tm* localtime(const std::time_t* time);:转换为std::tm类型相对于GMT偏移后的本地时
  • std::tm* gmtime(const std::time_t* time);:转换为std::tm类型的GMT标准世界时

如果想以字符串输出std::tm类型的对象,需要使用std::asctime函数:

  • char* asctime(const std::tm* time_ptr);

如果想自定义std::tm类型的对象输出字符串的格式,需要使用函数:

std::size_t strftime( char* str,           // 格式化后字符串保存的位置
                      std::size_t count,   // str 可以写入的最大字符个数
                      const char* format,  // 指定输出字符串的格式
                      const std::tm* tp ); // 要转换的 std::tm 对象

下面展示了以上方法的使用效果,包含了:

  • std::time_t类型转换为std::tm类型
  • 使用std::asctimestd::tm类型转换为固定的字符串格式
  • 使用std::strftimestd::tm类型转换为指定的格式
// 转换为本地时间
std::tm *local_tm = std::localtime(&cur_time);
// 将 std::tm 转换为字符串
char *local_tm_str = std::asctime(local_tm);
std::cout << "local_time: " << local_tm_str << std::endl;
// local_time: Sat Jul 27 19:47:32 2024
// 如果使用 gmtime 函数,得到的结果为
// gmt_time: Sat Jul 27 11:47:32 2024

// 自定义 std::tm 输出的字符串格式
char cus_local_time_str[100];
std::strftime(cus_local_time_str, sizeof(cus_local_time_str),
              "%Y-%m-%d %H:%M:%S", local_tm);
std::cout << "cus_local_time_str: " << cus_local_time_str << std::endl;
// cus_local_time_str: 2024-07-27 19:47:32
// 如果使用 gmtime 函数,得到的结果为
// cus_gmt_time_str: 2024-07-27 11:47:32

4.2 time_point类型

表示一个时间点,是一个包含两个模板参数的类,定义如下:

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

time_point类似于time_t,利用std::duration类型的对象保存从1970年开始,到达所表示时间点时长。因此,模板参数表示的意义为:

  • class Clock:表示时间点内相关数据的表示参考哪个时钟类
  • class Duration:表示时间点内数据保存的计时单元。从定义可以看出,默认使用的是Clock参数内的duration定义

time_point类型包含如下构造函数

  • time_point();:默认构造,表示1970年那个参考时间的时间点
  • time_point(const duration& d);:表示从1970年的参考时间开始,经过时间d后的时间点
  • time_point(time_point <Clock, Duration2>& t);:将t表示转换为duration对象后,构造一个时间点。使用这种方法要保证Duration2可以隐式转换为当前time_point内的duration的类型

通常,获取time_point最简单的方法就是使用时钟类的静态成员函数now

// def_tp 表示 1970年1月1日0点
std::chrono::time_point<std::chrono::system_clock> def_tp();
// cus_tp 表示 1970年1月1日0点经过 2s 后的时间
std::chrono::time_point<std::chrono::system_clock> cus_tp(std::chrono::seconds(2));
// now_tp 表示当前时间
auto now_tp = std::chrono::system_clock::now();

time_point常用的一个成员函数为:

duration time_since_epoch() const;
  • 返回duration表示的从1970年到time_point时间点的时长

示例:

// 返回时间点到 1970 年的时长
auto cus_tp_duration_to_epoch = cus_tp.time_since_epoch();
// 将时长转换为 ms
std::chrono::milliseconds cus_tp_ms = 
     std::chrono::duration_cast<std::chrono::milliseconds>(cus_tp_duration_to_epoch);
std::cout << cus_tp_ms.count() << std::endl; // 2000

time_point有很多重载的运算符,以实现对时间点的前后移动:

  • 自增++/自减--:对内部存储从1970到所表示时间点的duration的计时单元个数+/-1
  • 加减一个std::chrono::duration:返回相对于time_point所表示时间偏移duration后的新时间点
  • 两个time_point相减:返回duration所表示两个时间点之间的时长
  • 两个time_point做比较:通过time_since_epoch成员函数返回的结果做比较

注意:

  1. 以上重载的运算符如果涉及到两个time_point,必须保证两个time_point所关联的时钟类相同,即它们的第一个模板参数类型是同一个Clock
  2. 以上重载的运算符,如果涉及到duration作为操作数或返回值:
    • 如果是作为操作数,不要求duration的计时单元和time_point内部duration的计时单元相同
    • 如果返回一个duration的值,会根据duration的计时单元和time_point内部duration的计时单元的公共类型作为返回值duration的类型(即两者都可以隐式转换到的duration类型)

示例:

// 自定义时间点内部的 duration 长度
std::chrono::time_point<std::chrono::system_clock, std::chrono::seconds> 
                       tp_druation_second(std::chrono::seconds(5));
// 对内部的 duration 的计时单元数量 +1
++tp_druation_second;
std::cout << tp_druation_second.time_since_epoch() << std::endl; // 6s

// 测试时间点的移动
tp_druation_second += std::chrono::seconds(2);
std::cout << tp_druation_second.time_since_epoch() << std::endl; // 8s

// 创建新的时间点,返回新的时间点
// 递增 9.6s
auto tp_duration_second_new = tp_druation_second + 
                              std::chrono::duration<double, std::ratio<3, 1>>(3.2);
// 根据 C++ 20 下的输出,可以推测结果类型为 std::chronon::duration<double, std::chrono::seconds>
std::cout << tp_duration_second_new.time_since_epoch() << std::endl; // 17.6s

// 两个时间点相减,返回 duration
auto tp_duration_second_diff = tp_duration_second_new - tp_druation_second;
std::cout << tp_duration_second_diff << std::endl; // 9.6s

4.3 time_point转换ctime

只有std::chrono::system_clock提供了两个静态成员函数,用于std::chrono::system_clock::time_pointstd::time_t之间的类型转换:

  • std::time_t to_time_t(const time_point& t)
  • std::chrono::system_clock::time_point from_time_t( std::time_t t )

示例:

auto now_tp = std::chrono::system_clock::now();
// 将时间点转换为 time_t
std::time_t now_tp_time_t = std::chrono::system_clock::to_time_t(now_tp);
std::cout << std::ctime(&now_tp_time_t) << std::endl;
// Sun Jul 28 09:27:00 2024
  • 13
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值