聊聊C++标准库,准标准库中关于时间的概念和用法

概要

在实际C++业务开发中,经常需要使用系统API或者标准库去获取时间,计算时间的需求,其中,时间按概念又分时间段时间点;按表达形式又分系统时间本地时间;其实,获取到了时间,如何通过日志的方式把时间恰当表现出来

最近做一个定时,周循环类的多格式音频播放业务,因此,抽空系统了解了下这块,分享下。

标准库

打开标准库关于时间的命名空间chrono https://en.cppreference.com/w/cpp/chrono

可以看到三个概念:

  • clocks
  • time points
  • durations

时长很好理解,类比就是线段的长度,时间点也好理解,就是线段上的点,时长和时间段的关系是怎么样的,时间点靠时长加时间点(基准点)来体现,比如我们说9点钟了,那么其实就是说现在离凌晨已经过去九个小时了。如果能清晰理解这一点,那么就可以很容易看懂标准库,准标准库关于时间这一块的具体实现。

这还不够,时长我们是如何描述的,这也是个问题。想一想,如果要你去计时半分钟,你会怎么做。详细绝大多数人都会在心中默念三十秒;没有错,你所说的这三十秒确实是半分钟,但是如果让一个裁判去计时半分钟,他会拿出专业秒表来掐表。看出来了吧,这就是你对时长的描述,描述时长的精度不一样。你会以你认为的秒来作为单位计时,而裁判会用专业秒表的毫秒为单位来计时。虽然都是半分钟,量是相等的,但是你对半分钟的精度会差很多。所以,同理,计算机中对时长的描述也是有自己的单位的。而且单位也不是单一的。

因此,标准库中对时钟的定义需要满足 https://en.cppreference.com/w/cpp/named_req/Clock 这个定义。对于一个时钟,要有对于时长单位的精度period,以秒为单位,比如你想用毫秒作为单位,那就是std::chrono::millisecondsstd::ratio<1, 1000>秒,可以看下如下标准库节选代码:

// duration TYPES
using nanoseconds  = duration<long long, nano>;
using microseconds = duration<long long, micro>;
using milliseconds = duration<long long, milli>;
using seconds      = duration<long long>;
using minutes      = duration<int, ratio<60>>;
using hours        = duration<int, ratio<3600>>;


// SI TYPEDEFS
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>;

然后,时钟会有一个存储这么一个单位有多少的变量rep,然后时钟要告诉用户现在是啥时间点now(),以及这个时间点是否稳定(大小一旦确定下来会不会出现变化,比如系统时间的修改会不会影响)的变量is_steady

所以这三个概念是密不可分的,这个我们可以在标准库的实现中可以看到。

时钟

时钟会产生时间点(在这个时钟下的时间点);时钟会有精度,而精度这个概念又和时长有关。

标准库只提供了三个时钟,如下:

  • system_clock
  • steady_clock
  • high_resolution_clock

这几个时钟有啥不同呢,就体现在上面所说的关于时钟定义上面。

#define _XTIME_NSECS_PER_TICK   100

// CLOCKS
struct system_clock { // wraps GetSystemTimePreciseAsFileTime/GetSystemTimeAsFileTime
    using rep = long long;

    using period = ratio_multiply<ratio<_XTIME_NSECS_PER_TICK, 1>, nano>;

    using duration                  = chrono::duration<rep, period>;
    using time_point                = chrono::time_point<system_clock>;
    static constexpr bool is_steady = false;

    _NODISCARD static time_point now() noexcept { // get current time
        return time_point(duration(_Xtime_get_ticks()));
    }

    _NODISCARD static __time64_t to_time_t(const time_point& _Time) noexcept { // convert to __time64_t
        return static_cast<__time64_t>(_Time.time_since_epoch().count() / _XTIME_TICKS_PER_TIME_T);
    }

    _NODISCARD static time_point from_time_t(__time64_t _Tm) noexcept { // convert from __time64_t
        return time_point(duration(_Tm * _XTIME_TICKS_PER_TIME_T));
    }
};

struct steady_clock { // wraps QueryPerformanceCounter
    using rep                       = long long;
    using period                    = nano;
    using duration                  = nanoseconds;
    using time_point                = chrono::time_point<steady_clock>;
    static constexpr bool is_steady = true;

    _NODISCARD static time_point now() noexcept { // get current time
        const long long _Freq = _Query_perf_frequency(); // doesn't change after system boot
        const long long _Ctr  = _Query_perf_counter();
        static_assert(period::num == 1, "This assumes period::num == 1.");
        const long long _Whole = (_Ctr / _Freq) * period::den;
        const long long _Part  = (_Ctr % _Freq) * period::den / _Freq;
        return time_point(duration(_Whole + _Part));
    }
};

using high_resolution_clock = steady_clock;

这是Windows平台上标准库对这三个时钟的定义,可以发现,system_clock的精度是100ns,而steady_clock的精度是1ns;而再次查看注释,发现:

  • struct system_clock // wraps GetSystemTimePreciseAsFileTime/GetSystemTimeAsFileTime
  • struct steady_clock // wraps QueryPerformanceCounter

这是因为,Windows上获取系统的时间的这个API本身的精度就是100ns

Contains a 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601 (UTC).

从这个描述来看,也能推断出system_clock::is_steadyfalse,而steady_clock_clock获取的是硬件性能计数器的值,因此steady_clock::is_steadytrue

有趣的一点,Windows平台下,steady_clockhigh_resolution_clock是同一个东西。这和文档中的描述也是一样的,https://en.cppreference.com/w/cpp/chrono/high_resolution_clock

Class std::chrono::high_resolution_clock represents the clock with the smallest tick period provided by the implementation. It may be an alias of std::chrono::system_clock or std::chrono::steady_clock, or a third, independent clock.

high_resolution_clock拥有最小的时钟周期,也就是时间测量单位。由于Windows平台所能支持的最小时长测量单位是1ns,到顶了,因此steady_clockhigh_resolution_clock是同一个东西。

The high_resolution_clock is not implemented consistently across different standard library implementations, and its use should be avoided. It is often just an alias for std::chrono::steady_clock or std::chrono::system_clock, but which one it is depends on the library or configuration. When it is a system_clock, it is not monotonic (e.g., the time can go backwards). For example, for gcc’s libstdc++ it is system_clock, for MSVC it is steady_clock, and for clang’s libc++ it depends on configuration.

可以发现high_resolution_clock没有一个确定的实现。

所以标准库也给出了使用这几个时钟的建议:

Generally one should just use std::chrono::steady_clock or std::chrono::system_clock directly instead of std::chrono::high_resolution_clock: use steady_clock for duration measurements, and system_clock for wall-clock time.

尽量不要使用high_resolution_clock,需要测量时长使用steady_clock,需要获取目前的时间使用时钟system_clock

时长

可以看到,时间精度是个模板参数,表示它是个编译器的常量。因此确定时长需要精度;同时rep _rep表示量。这两个东西就构成了时长。其中精度在创建时长的时候是没用的(但是要指定),只要在调用时长转换时才有用,std::chrono::duration_cast

template <class _Rep, class _Period>
class duration { // represents a time duration
public:
    using rep    = _Rep;
    using period = typename _Period::type;

    static_assert(!_Is_duration_v<_Rep>, "duration can't have duration as first template argument");
    static_assert(_Is_ratio_v<_Period>, "period not an instance of std::ratio");
    static_assert(0 < _Period::num, "period negative or zero");

    constexpr duration() = default;

    template <class _Rep2,
        enable_if_t<is_convertible_v<const _Rep2&,
                        _Rep> && (treat_as_floating_point_v<_Rep> || !treat_as_floating_point_v<_Rep2>),
            int> = 0>
    constexpr explicit duration(const _Rep2& _Val) noexcept(
        is_arithmetic_v<_Rep>&& is_arithmetic_v<_Rep2>) // strengthened
        : _MyRep(static_cast<_Rep>(_Val)) {}

    template <class _Rep2, class _Period2,
        enable_if_t<
            treat_as_floating_point_v<
                _Rep> || (_Ratio_divide_sfinae<_Period2, _Period>::den == 1 && !treat_as_floating_point_v<_Rep2>),
            int> = 0>
    constexpr duration(const duration<_Rep2, _Period2>& _Dur) noexcept(
        is_arithmetic_v<_Rep>&& is_arithmetic_v<_Rep2>) // strengthened
        : _MyRep(chrono::duration_cast<duration>(_Dur).count()) {}

    _NODISCARD constexpr _Rep count() const noexcept(is_arithmetic_v<_Rep>) /* strengthened */ {
        return _MyRep;
    }
};

这段代码去掉了转换部分和重载运算符部分的代码。转换就是转换时长的精度而获得不同的count()

时间点

template <class _Clock, class _Duration = typename _Clock::duration>
class time_point { // represents a point in time
public:
    using clock    = _Clock;
    using duration = _Duration;
    using rep      = typename _Duration::rep;
    using period   = typename _Duration::period;

    static_assert(_Is_duration_v<_Duration>, "duration must be an instance of std::duration");

    constexpr time_point() = default;

    constexpr explicit time_point(const _Duration& _Other) noexcept(is_arithmetic_v<rep>) // strengthened
        : _MyDur(_Other) {}

    template <class _Duration2, enable_if_t<is_convertible_v<_Duration2, _Duration>, int> = 0>
    constexpr time_point(const time_point<_Clock, _Duration2>& _Tp) noexcept(
        is_arithmetic_v<rep>&& is_arithmetic_v<typename _Duration2::rep>) // strengthened
        : _MyDur(_Tp.time_since_epoch()) {}

    _NODISCARD constexpr _Duration time_since_epoch() const noexcept(is_arithmetic_v<rep>) /* strengthened */ {
        return _MyDur;
    }

    _CONSTEXPR17 time_point& operator+=(const _Duration& _Dur) noexcept(is_arithmetic_v<rep>) /* strengthened */ {
        _MyDur += _Dur;
        return *this;
    }

    _CONSTEXPR17 time_point& operator-=(const _Duration& _Dur) noexcept(is_arithmetic_v<rep>) /* strengthened */ {
        _MyDur -= _Dur;
        return *this;
    }

    _NODISCARD static constexpr time_point(min)() noexcept {
        return time_point((_Duration::min)());
    }

    _NODISCARD static constexpr time_point(max)() noexcept {
        return time_point((_Duration::max)());
    }

private:
    _Duration _MyDur{duration::zero()}; // duration since the epoch
};

可以看到,时间点的成员变量就是时间段,同时,时间点的duration默认是时钟的duration。这里就可以看到关联了。

应用

理解了上面这三个概念,你就会使用chrono空间中的大部分函数了。

获取一个时间戳

#define VAR(var) std::setw(12) << std::right << (#var) << " : " << std::left << (var) << std::endl

auto yearStamp = std::chrono::duration_cast<std::chrono::duration<long long, std::ratio<60 * 60 * 24 * 365>>>(
    std::chrono::system_clock::now().time_since_epoch()).count();
auto dayStamp = std::chrono::duration_cast<std::chrono::duration<long long, std::ratio<60 * 60 * 24>>>(
    std::chrono::system_clock::now().time_since_epoch()).count();
auto hourStamp = std::chrono::duration_cast<std::chrono::hours>(
    std::chrono::system_clock::now().time_since_epoch()).count();
auto secondStamp = std::chrono::duration_cast<std::chrono::seconds>(
    std::chrono::system_clock::now().time_since_epoch()).count();
auto centiStamp = std::chrono::duration_cast<std::chrono::duration<long long, std::centi>>(
    std::chrono::system_clock::now().time_since_epoch()).count();
auto millStamp = std::chrono::duration_cast<std::chrono::milliseconds>(
    std::chrono::system_clock::now().time_since_epoch()).count();
auto microStamp = std::chrono::duration_cast<std::chrono::microseconds>(
    std::chrono::system_clock::now().time_since_epoch()).count();
auto systemStamp = std::chrono::system_clock::now().time_since_epoch().count();
auto systemStamp2 = std::chrono::duration_cast<std::chrono::duration<
	long long, std::ratio_multiply<std::ratio<100>, std::nano>>>(
    std::chrono::system_clock::now().time_since_epoch()).count();
auto nanoStamp = std::chrono::duration_cast<std::chrono::nanoseconds>(
    std::chrono::system_clock::now().time_since_epoch()).count();

std::cout << std::endl
    << VAR(yearStamp)
    << VAR(dayStamp)
    << VAR(hourStamp)
    << VAR(secondStamp)
    << VAR(millStamp)
    << VAR(microStamp)
    << VAR(systemStamp)
    << VAR(systemStamp2)
    << VAR(nanoStamp);
// output

   yearStamp : 50
    dayStamp : 18466
   hourStamp : 443186
 secondStamp : 1595473015
   millStamp : 1595473015669
  microStamp : 1595473015669220
 systemStamp : 15954730156692205
systemStamp2 : 15954730156692206
   nanoStamp : 1595473015669220800

转换是正确的,因为2020年离1970年确实相差50年;其次,业务开发中绝大多数场景中只会用到秒级时间戳和毫秒级时间戳。后面的

通过一个时间戳得到时间点

上游给你一个秒级时间戳,你如何转换到chrono::time_point来应用到自己的业务中。

std::string stamp = "1595473015";
auto point = std::chrono::time_point<
	std::chrono::system_clock, std::chrono::seconds>(
	std::chrono::seconds(std::stoll(stamp)));

如何在日志中表示时长

通常来说会在日志中把时长转换成秒输出来,但是这样会有一个不好的地方,看日志的人难受啊,我咋知道这些秒对应的是多少小时多少分钟,如果转换成分钟,小时,那么又不会精确。

最好是能输出如下形式,d/hh:mm:ss.cc,这就很舒服了。

template <class CharT, class Traits, class Rep, class Period>
std::basic_ostream<CharT, Traits>&
operator <<(
    std::basic_ostream<CharT, Traits>& os, std::chrono::duration<Rep, Period> d)
{
    typedef std::chrono::duration<long long, std::ratio<86400> > days;
    typedef std::chrono::duration<long long, std::centi> centiseconds;

    centiseconds cs = std::chrono::duration_cast<centiseconds>(d);
    if (d - cs > std::chrono::milliseconds(5)
        || (d - cs == std::chrono::milliseconds(5) && cs.count() & 1))
        ++cs;
    std::chrono::seconds s = std::chrono::duration_cast<std::chrono::seconds>(cs);
    cs -= s;
    std::chrono::minutes m = std::chrono::duration_cast<std::chrono::minutes>(s);
    s -= m;
    std::chrono::hours h = std::chrono::duration_cast<std::chrono::hours>(m);
    m -= h;
    days dy = std::chrono::duration_cast<days>(h);
    h -= dy;

    // print d/hh:mm:ss.cc
    os << dy.count() << '/' << std::setfill('0')
        << std::setw(2) << h.count() << ':'
        << std::setw(2) << m.count() << ':'
        << std::setw(2) << s.count() << ':'
        << std::setw(2) << cs.count();
    return os;
}

这里使用时长精度的转换然后求差值来达到目的,并重载输出运算符,方便输出;如果标准库中duration重载了%运算符,那么这个目的更好达到,所以,在Boost中,duration实现了重载%运算符。

std::time_t

这是STL和C-style函数的桥梁变量了。

  • std::chrono::system_clock::to_time_t
  • std::chrono::system_clock::from_time_t

下面是获取秒级时间戳的三种方法:

auto chronoStamp = std::chrono::duration_cast<std::chrono::seconds>(
    std::chrono::system_clock::now().time_since_epoch()).count();

auto chronoTimet = std::chrono::system_clock::to_time_t(
    std::chrono::system_clock::now());

auto cstyleStamp = std::time(nullptr);

std::cout << chronoStamp << std::endl
    << chronoTimet << std::endl
    << cstyleStamp << std::endl;

结果就不展示了,三个结果都是一样的。

时间点的展示

std::chrono::system_clock::to_time_t
std::chrono::system_clock::frome_time_t
std::time
std::localtime
std::gmtime
std::mktime
std::put_time
std::asctime
std::strftime
std::ctime
std::get_time
std::chrono::time_point
std::time_t
std::tm
std::string

这里要注意std::localtimestd::gmtime的区别,给个链接:https://en.cppreference.com/w/cpp/chrono/c/gmtime

准标准库

Boost.Chrono,boost关于chrono的实现的代码还是挺好看的,它比标准库多实现了如下的时钟

  • process_real_cpu_clock
  • process_user_cpu_clock
  • process_system_cpu_clock
  • process_cpu_clock
  • thread_clock

所利用的API:各种时钟,各种平台的实现API

其次,它很好的实现了durationtime_point的输出

#include <iostream>
#include <boost/chrono/chrono_io.hpp>

int main()
{
    using namespace std;
    using namespace boost;

    cout << "milliseconds(1) = "
         <<  boost::chrono::milliseconds(1) << '\n';

    cout << "milliseconds(3) + microseconds(10) = "
         <<  boost::chrono::milliseconds(3) + boost::chrono::microseconds(10) << '\n';

    cout << "hours(3) + minutes(10) = "
         <<  boost::chrono::hours(3) + boost::chrono::minutes(10) << '\n';

    typedef boost::chrono::duration<long long, boost::ratio<1, 2500000000> > ClockTick;
    cout << "ClockTick(3) + boost::chrono::nanoseconds(10) = "
         <<  ClockTick(3) + boost::chrono::nanoseconds(10) << '\n';

   // ...
    return 0;
}

这部分大家可以看下源码或者文档,输出这块也用到了C++流的一些技巧。

之前提到准标准库duration重载了%运算符,也可以实现duration的格式化。

class ClockTime
{
    typedef boost::chrono::hours hours;
    typedef boost::chrono::minutes minutes;
    typedef boost::chrono::seconds seconds;
public:
    hours hours_;
    minutes minutes_;
    seconds seconds_;

    template <class Rep, class Period>
      explicit ClockTime(const boost::chrono::duration<Rep, Period>& d)
        : hours_  (boost::chrono::duration_cast<hours>  (d)),
          minutes_(boost::chrono::duration_cast<minutes>(d % hours(1))),
          seconds_(boost::chrono::duration_cast<seconds>(d % minutes(1)))
          {}
};

总的来说,准标准库包含了标准库的东西,而且,准标准库对于时间这块也提供了更多的工具,更好的格式输出,用就完事了。

业务开发中用到的一些东西

贴一些业务代码吧,分享下。

#include <cxx-prettyprint>
// Usage Link : http://louisdx.github.io/cxx-prettyprint/

#define BY(var) "(" << var << ")"
#define BJ(var) "<" << var << ">"
#define BF(var) "[" << var << "]"
#define BH(var) "{" << var << "}"

void LocalPlanController::FireRingWeek(const PlanListNode& node)
{
    using namespace std::chrono_literals;

    std::vector<std::string> logs;
    std::stringstream LG;

    for (auto it = node.operationList.begin(); it != node.operationList.end(); ++it)
    {
        if (it->operationStatus == OperationStatus::kClose)
            continue;

        auto ctime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
        auto wday = std::localtime(&ctime)->tm_wday;

        LG << "\n  + name " << BJ(it->name) << " | operationLogId " << BJ(it->operationId)
            << " | duration " << BJ(it->duration) << " | mode " << BJ(it->week)
            << " | s - e " << BF(std::make_pair(it->startTime, it->endTime));

        for (auto i = wday; i < wday + WEEK_DAYS; ++i)
        {
            if (!it->week.test(i % WEEK_DAYS))
                continue;

            auto beforeTime = std::chrono::system_clock::now();
            auto afterTime = std::chrono::system_clock::from_time_t(std::mktime(&FormatTime(it->startTime))) + DAY_HOURS * (i - wday);

            if (afterTime <= beforeTime)
                continue;

            auto duration = std::chrono::duration_cast<std::chrono::seconds>(afterTime - beforeTime);

            LG << "\n   - " << "wday | " << BJ(i % WEEK_DAYS) << " will start after " << BF(duration);

            RingInfo info;
            info.audioFile = it->audioFileDtoList.front();
            info.duration = it->duration;
            info.operationLogId = it->operationId;
            info.outputSourceList = it->outputSourceList;
            info.traceId = Zeus::Uuid::GenerateRandom().toString("");

            _relativeTimer->AddTimerTask(it->operationId, std::bind(&LocalPlanController::RingWeekLoop, this, info), duration);
        }

        logs.push_back(LG.str());
        LG.clear();
    }

    LOG_INFO << BH(node.tslKey) << " | model " << BF(node.timeMode) << logs;
}


tm LocalPlanController::FormatTime(const std::string& fm)
{
    auto nowTime = std::time(nullptr);
    auto tm = std::localtime(&nowTime);
    std::stringstream ss;
    ss << fm;
    tm->tm_sec = 0;
    (ss >> tm->tm_hour).ignore() >> tm->tm_min;
    ss.clear();

    return *tm;
}

一些日志:

[2020-07-21 15:58:13.541][22420][INFO][local_plan_controller.cpp.LocalPlanController::FireRingWeek:435][-] {ringing} | model [2][
  + name <打铃计划> | operationLogId <512392619394445312> | duration <20> | mode <1111111> | s - e [(15:59, )]
   - wday | <2> will start after [0/00:0:46:0]
   - wday | <3> will start after [1/00:0:46:0]
   - wday | <4> will start after [2/00:0:46:0]
   - wday | <5> will start after [3/00:0:46:0]
   - wday | <6> will start after [4/00:0:46:0]
   - wday | <0> will start after [5/00:0:46:0]
   - wday | <1> will start after [6/00:0:46:0], 
  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值