如何度量程序在某一时刻的时间?
通常,我们用时刻来表示,比如"2022-02-26 23:43:00.000000",这种方式便于人查看,但不便于程序中的比较和计算。比如有2个时刻A和B,计算哪个时刻在前,哪个在后,或者要计算时刻A和B的时间差时,这种字符串表示方式就很麻烦。
我们想到将字符串形式的时刻,用自纪元时间(Epoch时间,1970-01-01 00:00:00 +0000 (UTC))以来的时间戳来表示,精度为1us(微秒)。
Linux中,如何获取这个时间呢?
使用gettimeofday(2),分辨率1us,其实现也能达到毫秒级(当然分辨率不等于精度),再加上Linux是非实时任务系统,也能满足日常计时功能。
前面讲过,time(2)只能精确到1s,ftime(3)已被废弃,clock_gettime(2)精度高,但系统调用开销比gettimeofday(2)大,网络编程中,最适合用gettimeofday(2)来计时。muduo中也是这么做的。
有没有一种可能,两个线程,或者两段出现在1us内执行?
答案是有可能的,对于常规情况,即使时间戳相同,并不影响我们的日常计时功能;对于特殊需求,比如排序、查找,需要区分时间戳大小的,后面遇到具体情况具体分析。
Timestamp类设计
由于时间戳希望在不同变量之间赋值、拷贝,因此设计成值语义的,继承自copyable class。
数据成员:
成员变量microSecondsSinceEpoch_,用来来表示从 Epoch时间到目前为止的微妙数,初值0(也表示无效值)。
microSecondsSinceEpoch_的数据类型为什么是int64_t,而不是int32_t或者uint64_t?因为32位连一年的微妙数都不能表示,而int64_t可以表示290余年的微妙数(一年按365243600*100000计算),未来还能表示一百余年,也就是说,其范围满足目前日常需求。而有符号的int64_t可以用来让2个时间戳进行差值计算,从而表示先后顺序。当然,时间戳本身为负数没有意义。
构造函数(ctor)
可以像这样定义Timstamp及其构造函数:
/**
* 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对象。
// Timestamp.h
bool valid() const
{ return microSecondsSinceEpoch_ > 0; }
static Timestamp invalid()
{
return Timestamp();
}
1)为什么需要invalid()?
有时,我们需要一个临时的Timestamp对象,并不会用它来表示真实的时间,而是表示无用Timestamp对象。此时,用invalid()类函数自然是没问题的。当然,也可以用default ctor来构造一个临时Timestamp对象也是可以的,invalid()实现也是这么做的,但invalid()语义更清晰,而且不依赖于default ctor实现。假设哪天修改了microSecondsSinceEpoch_含义,那么用default ctor来代表无效Timestamp对象,也就失效了,然而invalid()接口却可以不变,客户端不用修改代码。
2)为什么invalid()是static?
因为要构造一个Timestamp对象,也就是说,此时还没有Timestamp对象,也就无法通过对象的成员函数来构造自身对象。
获得当前时间(时刻)
Timestamp的一个重要意义,在于捕获当前时间,转换为时间戳反馈给调用者。
// Timestamp.h
/**
* Get time of now
*/
static Timestamp now();
static const int kMicroSecondsPerSecond = 1000 * 1000; // 1s = 1e6 us
// Timestamp.cc
Timestamp Timestamp::now()
{
struct timeval tv;
gettimeofday(&tv, NULL);
int64_t seconds = tv.tv_sec;
return Timestamp(seconds * kMicroSecondsPerSecond + tv.tv_usec);
}
1)用gettimeofday()获取当前时刻,转化为微秒,并构造一个Timestamp临时对象。1换算公式:sec = 1e6 usec
时间换算
如何将由time(2)获得的自Epoch时间(1970-01-01 00:00:00 +0000 (UTC).)以来的秒数(time_t类型),转化为Timestamp类型对象?
可以定义fromUnixTime来完成这个工作:
// 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);
}
1)第一个重载版本,只转换提供的秒数,微秒数默认0;第二个版本,提供了秒数和微秒数的设置
对象交换
有时为了避免对象数据成员的拷贝,会利用swap对对象进行交换操作。
// Timestamp.h
void swap(Timestamp& that)
{
std::swap(microSecondsSinceEpoch_, that.microSecondsSinceEpoch_);
}
1)就目前的设计来说,完全可以用std::swap来交换2个对象,而不用定义Timestamp::swap()。这里是为了以后方便扩展,自定义swap行为。
获取时间戳
获取从Epoch时间,到目前为止的时间戳数值
// Timestamp.h
int64_t microSecondsSinceEpoch() const { return microSecondsSinceEpoch_;}; // 微秒数
time_t secondsSinceEpoch() const // 秒数
{
return static_cast<time_t>(microSecondsSinceEpoch_ / kMicroSecondsPerSecond);
}
1)获取微秒数;
2)获取秒数;
获取可打印字符串
// 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函数来实现:
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)addTime() 将一个Timestamp加上指定时间;
由于toString()和 toFormattedString() 可以输出类的信息,因此可以作为测试时判断的依据。
Timestamp的单元测试,可以这样设计:
// Timestamp_unittest.cc
int main()
{
Timestamp now(Timestamp::now());
printf("%s\n", now.toString().c_str()); // 测试now() + copy ctor
passByValue(now); // 测试值传递
passByConstReference(now); // 测试引用传递
benchmark();
return 0;
}
void passByValue(Timestamp x)
{
printf("%s\n", x.toString().c_str());
}
void passByConstReference(const Timestamp& x)
{
printf("%s\n", x.toString().c_str());
}
void benchmark()
{
const int kNumber = 1000*1000;
std::vector<Timestamp> stamps;
stamps.reserve(kNumber);
for (int i = 0; i < kNumber; ++i) {
stamps.push_back(Timestamp::now());
}
printf("%s\n", stamps.front().toString().c_str());
printf("%s\n", stamps.back().toString().c_str());
printf("%f\n", muduo::timeDifference(stamps.back(), stamps.front()));
int increments[100] = { 0 };
int64_t start = stamps.front().microSecondsSinceEpoch();
for (int i = 0; i < kNumber; ++i) {
int64_t next = stamps[i].microSecondsSinceEpoch();
int64_t inc = next - start;
start = next;
if (inc < 0)
{
printf("reverse!\n");
}
else if (inc < 100)
{
++increments[inc];
}
else
{
printf("big gap %d\n", static_cast<int>(inc));
}
}
for (int i = 0; i < 100; ++i) {
printf("%2d: %d\n", i, increments[i]);
}
}
运行结果:
1645946164.776317
1645946164.776317
1645946164.776317
1645946164.777235
1645946164.981893
0.204658
big gap 109
big gap 193
big gap 162
big gap 158
big gap 35248
big gap 7007
big gap 142
big gap 6098
big gap 2142
big gap 12422
big gap 262
big gap 12291
big gap 12078
big gap 222
big gap 12069
big gap 229
0: 901839
1: 97441
2: 83
3: 191
4: 175
5: 13
6: 47
7: 22
8: 10
9: 6
10: 7
11: 10
12: 4
13: 12
14: 4
15: 4
16: 5
17: 10
18: 2
19: 2
20: 1
21: 0
22: 2
...
小结
1)有些重要函数功能点,并未测试到,比如toFormattedString();
2)针对特定函数,设计的测试用例并不全面,比如并没有永非法的时间戳数值(如<0),来验证生成的Timestamp有效性。