muduo库-时间戳类Timestamp

muduo库-时间戳类Timestamp

如何度量程序在某一时刻的时间?

通常,我们用时刻来表示,比如"2023-06-26 23:43:00.000000",这种方式便于人查看,但不便于程序中的比较和计算。比如有2个时刻A和B,计算哪个时刻在前,哪个在后,或者要计算时刻A和B的时间差时,这种字符串表示方式就很麻烦。

我们想到将字符串形式的时刻,用自纪元时间(Epoch时间,1970-01-01 00:00:00 +0000 (UTC))以来的时间戳来表示,精度为1us(微秒)。

Linux中,如何获取这个时间呢?

使用gettimeofday,分辨率1us,其实现也能达到毫秒级(当然分辨率不等于精度),再加上Linux是非实时任务系统,也能满足日常计时功能。 前面讲过,time只能精确到1s,ftime已被废弃,clock_gettime精度高,但系统调用开销比gettimeofday大,网络编程中,最适合用gettimeofday来计时。muduo中也是这么做的。

有没有一种可能,两个线程,或者两段出现在1us内执行?答案是有可能的,对于常规情况,即使时间戳相同,并不影响我们的日常计时功能;对于特殊需求,比如排序、查找,需要区分时间戳大小的,后面遇到具体情况具体分析。

Timestamp类

由于时间戳希望在不同变量之间赋值、拷贝,因此设计成值语义的,继承自copyable class。

数据成员:

成员变量microSecondsSinceEpoch_,用来来表示从 Epoch时间到目前为止的微妙数,初值0(也表示无效值)。

microSecondsSinceEpoch_的数据类型为什么是int64_t,而不是int32_t或者uint64_t?因为32位连一年的微妙数都不能表示,而int64_t可以表示290余年的微妙数(一年按\(365243600 * 100000\)计算),未来还能表示一百余年,也就是说,其范围满足目前日常需求。而有符号的int64_t可以用来让2个时间戳进行差值计算,从而表示先后顺序。当然,时间戳本身为负数没有意义。

构造函数

可以像这样定义Timstamp及其构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* Time stamp in UTC, in microseconds resolution.
*/
class Timestamp : public copyable
{
public:
/**
* Constructs an invalid Timestamp
*/
Timestamp() : microSecondsSinceEpoch_(0)
{ }

/**
* Constructs a Timestamp at specific time
* @param microSecondsSinceEpochArg
*/
explicit Timestamp(int64_t microSecondsSinceEpochArg)
: microSecondsSinceEpoch_(microSecondsSinceEpochArg)
{
}
...

private:
int64_t microSecondsSinceEpoch_;
};
  1. 继承自copyable,表明这是一个值语义的class,其对象能够进行copy操作;
  2. default ctor(构造函数),存储时间戳变量microSecondsSinceEpoch_初值0,0和负数都表示无效值。同时,也提供单一参数版本ctor,给调用者构造指定时间戳值的Timestamp对象的机会。

对象有效性

至于microSecondsSinceEpoch_符号,我们可以定义成员函数valid()判断其有效性,通过invalid()构造一个无效的Timestamp对象。

1
2
3
4
5
6
7
8
9
10
// Timestamp.h
bool valid() const
{
return microSecondsSinceEpoch_ > 0;
}

static Timestamp invalid()
{
return Timestamp();
}

gettimeofday()获取当前时刻,转化为微秒,并构造一个Timestamp临时对象。1换算公式:sec = 1e6 usec

时间换算

如何将由time()获得的自Epoch时间(1970-01-01 00:00:00 +0000 (UTC).)以来的秒数(time_t类型),转化为Timestamp类型对象?

可以定义fromUnixTime来完成这个工作:

1
2
3
4
5
6
7
8
9
// Timestamp.h
static Timestamp fromUnixTime(time_t t)
{
return fromUnixTime(t, 0);
}
static Timestamp fromUnixTime(time_t t, int microseconds)
{
return Timestamp(static_cast<int64_t>(t) * kMicroSecondsPerSecond + microseconds);
}

第一个重载版本,只转换提供的秒数,微秒数默认0;第二个版本,提供了秒数和微秒数的设置

对象交换

有时为了避免对象数据成员的拷贝,会利用swap对对象进行交换操作。

1
2
3
4
5
// Timestamp.h
void swap(Timestamp& that)
{
std::swap(microSecondsSinceEpoch_, that.microSecondsSinceEpoch_);
}

就目前的设计来说,完全可以用std::swap来交换2个对象,而不用定义Timestamp::swap()。这里是为了以后方便扩展,自定义swap行为。

获取时间戳

获取从Epoch时间,到目前为止的时间戳数值

1
2
3
4
5
6
// Timestamp.h
int64_t microSecondsSinceEpoch() const { return microSecondsSinceEpoch_;}; // 微秒数
time_t secondsSinceEpoch() const // 秒数
{
return static_cast<time_t>(microSecondsSinceEpoch_ / kMicroSecondsPerSecond);
}

获取可打印字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Timestamp.h
std::string toString() const;
std::string toFormattedString(bool showMicroseconds = true) const;

#ifndef __STDC_FORMAT_MACROS // PRId64, for printf data in cross platform
#define __STDC_FORMAT_MACROS
#endif

#include <inttypes.h>

string Timestamp::toString() const
{
char buf[32] = {0};
int64_t seconds = microSecondsSinceEpoch_ / kMicroSecondsPerSecond;
int64_t microseconds = microSecondsSinceEpoch_ % kMicroSecondsPerSecond;
snprintf(buf, sizeof(buf), "%" PRId64 ".%06" PRId64 "", seconds, microseconds);
return buf;
}
string Timestamp::toFormattedString(bool showMicroseconds) const
{
char buf[64] = {0};
time_t seconds = static_cast<time_t>(microSecondsSinceEpoch_ / kMicroSecondsPerSecond);
struct tm tm_time;
gmtime_r(&seconds, &tm_time); // convert seconds since Epoch to UTC time (struct tm)

if (showMicroseconds)
{
int microseconds = static_cast<int>(microSecondsSinceEpoch_ % kMicroSecondsPerSecond);
snprintf(buf, sizeof(buf), "%4d%02d%02d %02d:%02d:%02d.%06d",
tm_time.tm_year + 1990, tm_time.tm_mon + 1, tm_time.tm_mday,
tm_time.tm_hour, tm_time.tm_min, tm_time.tm_sec,
microseconds);
}
else
{
snprintf(buf, sizeof(buf), "%4d%02d%02d %02d:%02d:%02d",
tm_time.tm_year + 1990, tm_time.tm_mon + 1, tm_time.tm_mday,
tm_time.tm_hour, tm_time.tm_min, tm_time.tm_sec);
}
return buf;
}
  1. toString() 将秒数、微秒数转换为可打印的std::string类型,用PRId64跨平台输出64bit数据到string缓存;
  2. toFormattedString()将时间戳转换为人类可理解的格式化时间字符串,形如"yyyymmdd hh:mm:ss.zzzzzz"。

辅助函数(非class member函数)

常需要比较2个时间先后顺序,计算这2个时刻之间的时间差,一个时刻加上一段时间来得到另外一个时刻,可以通过定义helper函数来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
inline bool operator<(Timestamp lhs, Timestamp rhs)
{
return lhs.microSecondsSinceEpoch() < rhs.microSecondsSinceEpoch();
}

/**
* Gets time difference of two timestamps, result in seconds.
* @param high
* @param low
* @return (high - low) in seconds.
* @c double has 52-bit precision, enough for one-microsecond
* resolution for next 100 years.
*/
inline double timeDifference(Timestamp high, Timestamp low)
{
int64_t diff = high.microSecondsSinceEpoch() - low.microSecondsSinceEpoch();
return static_cast<double>(diff) / Timestamp::kMicroSecondsPerSecond;
}

/**
* Add @c seconds to given timestamp.
* @param timestamp given basic timestamp
* @param seconds given seconds to be added to timestamp
* @return timestamp + seconds as Timestamp
*/
inline Timestamp addTime(Timestamp timestamp, double seconds)
{
int64_t delta = static_cast<int64_t>(seconds * Timestamp::kMicroSecondsPerSecond);
return Timestamp(timestamp.microSecondsSinceEpoch() + delta);
}
  1. operator<() 除了比较2个时间戳大小关系(代表的先后顺序),也是实现等价关系判断的重要条件;
  2. timeDifference()计算2个时间戳差值,精确到1usec,用小数表示,而整数部分表示1sec;
  3. addTime() 利用一个基准时间戳timestamp + 时间段seconds(秒数),得到新的Timestamp对象。

单元测试

单元测试测什么?muduo是以class为单位,根据提供给用户的功能点进行测试。有些进行的是覆盖测试。

Timestamp主要功能点:

  1. 构造对象:默认对象,无效对象;
  2. 值语义,即引用传递、值传递对象;
  3. now()获取当前时间;
  4. microSecondsSinceEpoch()获取微秒数,secondsSinceEpoch()获取秒数;
  5. valid()判断对象是否有效;
  6. fromUnixTime() 将Epoch时间转换为Timestamp对象;
  7. toString() 将时间戳转换为string类型;
  8. toFormattedString() 将时间戳转换为格式化字符串string类型;

辅助函数主要功能点:

  1. operator<() 比较2个Timestamp对象大小;
  2. timeDifference()计算2个Timestamp对象差值;
  3. ddTime() 将一个Timestamp加上指定时间;

由于toString()toFormattedString()可以输出类的信息,因此可以作为测试时判断的依据。