在实际C++业务开发中,经常需要使用系统API或者标准库去获取时间,计算时间的需求,其中,时间按概念又分时间段,时间点;按表达形式又分系统时间,本地时间;其实,获取到了时间,如何通过日志的方式把时间恰当表现出来。
最近做一个定时,周循环类的多格式音频播放业务,因此,抽空系统了解了下这块,分享下。
打开标准库关于时间的命名空间chrono https://en.cppreference.com/w/cpp/chrono
可以看到三个概念:
时长很好理解,类比就是线段的长度,时间点也好理解,就是线段上的点,时长和时间段的关系是怎么样的,时间点靠时长加时间点(基准点)来体现,比如我们说9点钟了,那么其实就是说现在离凌晨已经过去九个小时了。如果能清晰理解这一点,那么就可以很容易看懂标准库,准标准库关于时间这一块的具体实现。
这还不够,时长我们是如何描述的,这也是个问题。想一想,如果要你去计时半分钟,你会怎么做。详细绝大多数人都会在心中默念三十秒;没有错,你所说的这三十秒确实是半分钟,但是如果让一个裁判去计时半分钟,他会拿出专业秒表来掐表。看出来了吧,这就是你对时长的描述,描述时长的精度不一样。你会以你认为的秒来作为单位计时,而裁判会用专业秒表的毫秒为单位来计时。虽然都是半分钟,量是相等的,但是你对半分钟的精度会差很多。所以,同理,计算机中对时长的描述也是有自己的单位的。而且单位也不是单一的。
因此,标准库中对时钟的定义需要满足 https://en.cppreference.com/w/cpp/named_req/Clock 这个定义。对于一个时钟,要有对于时长单位的精度period
,以秒为单位,比如你想用毫秒作为单位,那就是std::chrono::milliseconds
即std::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
。
所以这三个概念是密不可分的,这个我们可以在标准库的实现中可以看到。
时钟会产生时间点(在这个时钟下的时间点);时钟会有精度,而精度这个概念又和时长有关。
标准库只提供了三个时钟,如下:
这几个时钟有啥不同呢,就体现在上面所说的关于时钟定义上面。
#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;而再次查看注释,发现:
这是因为,Windows上获取系统的时间的这个API本身的精度就是100ns,
Contains a 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601 (UTC).
从这个描述来看,也能推断出system_clock::is_steady
为false,而steady_clock_clock
获取的是硬件性能计数器的值,因此steady_clock::is_steady
为true。
有趣的一点,Windows平台下,steady_clock
和high_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_clock
和high_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::localtime
和std::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
其次,它很好的实现了duration
和time_point
的输出
#include
#include
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
// 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],