C++ `std::chrono` 高级:自定义时钟与时间点操作

好的,各位朋友,欢迎来到今天的“C++ std::chrono 高级:自定义时钟与时间点操作”特别节目!我是你们的老朋友,BUG 终结者。今天我们要聊聊 C++11 引入的 std::chrono 库的深水区,尤其是如何玩转自定义时钟和时间点。

std::chrono 的基本回顾:时间,时长,时钟

在深入之前,我们先简单回顾一下 std::chrono 的三大基石:

  • Duration(时长): 代表时间段,比如 5 秒,10 分钟。
  • Time Point(时间点): 代表时间轴上的一个特定时刻,比如 2023 年 10 月 27 日 10:00:00。
  • Clock(时钟): 提供当前时间点,以及时间单位的信息。

这三者之间的关系,就像是:

概念 类比 作用
时长 一段旅程 表示时间的长度,比如“开车开了 3 个小时”
时间点 地标 时间轴上的一个具体位置,比如“到达了长城”
时钟 指南针/GPS 提供当前位置(时间点),以及测量距离(时长)的工具,比如“现在几点”、“走了多久”

为什么要自定义时钟?

标准库已经提供了 system_clock, steady_clock, high_resolution_clock 等时钟。那为什么我们还需要自定义时钟呢?原因有很多:

  • 模拟时间: 在单元测试中,我们可能需要控制时间流逝,以便测试时间相关的逻辑。
  • 硬件时钟: 某些嵌入式系统或专用硬件可能提供自己的时钟源,需要通过自定义时钟来访问。
  • 逻辑时钟: 在分布式系统中,可能需要使用逻辑时钟来保证事件的顺序,而不是依赖物理时间。
  • 性能优化: 标准时钟可能不满足某些特定场景下的性能需求,自定义时钟可以进行优化。

自定义时钟的骨架:

要自定义一个时钟,我们需要定义一个类,并满足 std::chrono 的时钟概念 (Clock concept) 的要求。 简单来说,需要满足以下条件:

  1. typedef 成员:

    • rep: 表示时钟 tick 的数值类型,通常是 int, long long 等。
    • period: std::ratio 类型,表示时钟 tick 的周期。例如,std::ratio<1, 1000> 表示 1 毫秒。
    • duration: std::chrono::duration<rep, period>,表示时钟 tick 的时长。
    • time_point: std::chrono::time_point<clock_type>,表示时钟的时间点。
  2. 静态成员函数 now():

    • 返回当前时间点 (time_point)。
  3. (可选) 静态常量 is_steady:

    • true 表示时钟是稳定的 (steady),即时间不会倒流。 false 表示时钟可能不稳定 (比如 system_clock)。

一个简单的自定义时钟示例:

让我们先从一个最简单的自定义时钟开始,它仅仅是一个墙上时钟,以秒为单位:

#include <iostream>
#include <chrono>

class WallClock {
public:
    using rep = long long;
    using period = std::ratio<1>; // 秒
    using duration = std::chrono::duration<rep, period>;
    using time_point = std::chrono::time_point<WallClock>;
    static constexpr bool is_steady = false; // 墙上时钟可能被手动调整

    static time_point now() noexcept {
        auto now = std::chrono::system_clock::now().time_since_epoch();
        auto seconds = std::chrono::duration_cast<std::chrono::seconds>(now).count();
        return time_point(duration(seconds));
    }
};

int main() {
    WallClock::time_point now = WallClock::now();
    std::cout << "WallClock time: " << now.time_since_epoch().count() << " seconds" << std::endl;

    return 0;
}

在这个例子中:

  • 我们定义了一个名为 WallClock 的类。
  • replong longperiodstd::ratio<1> (表示秒)。
  • now() 函数返回当前时间点,它从 system_clock 获取当前时间,然后转换为秒。
  • is_steady 被设置为 false,因为墙上时钟可能会被手动调整。

模拟时钟:单元测试的利器

接下来,我们创建一个模拟时钟,这在单元测试中非常有用,因为我们可以控制时间流逝:

#include <iostream>
#include <chrono>

class MockClock {
public:
    using rep = long long;
    using period = std::ratio<1>; // 秒
    using duration = std::chrono::duration<rep, period>;
    using time_point = std::chrono::time_point<MockClock>;
    static constexpr bool is_steady = true; // 模拟时钟应该是稳定的

    static void set_now(time_point tp) {
        current_time = tp;
    }

    static time_point now() noexcept {
        return current_time;
    }

private:
    static time_point current_time;
};

MockClock::time_point MockClock::current_time = MockClock::time_point(MockClock::duration(0)); // 初始化为 0

int main() {
    MockClock::time_point start = MockClock::now();
    std::cout << "MockClock start time: " << start.time_since_epoch().count() << " seconds" << std::endl;

    MockClock::set_now(MockClock::time_point(MockClock::duration(10))); // 设置时间为 10 秒
    MockClock::time_point now = MockClock::now();
    std::cout << "MockClock current time: " << now.time_since_epoch().count() << " seconds" << std::endl;

    return 0;
}

在这个例子中:

  • 我们添加了一个 set_now() 函数,允许我们设置模拟时钟的当前时间。
  • current_time 是一个静态成员变量,用于存储当前时间。
  • is_steady 被设置为 true,因为模拟时钟应该是稳定的。

高精度时钟:追求极致

如果你需要比标准时钟更高的精度,你可以尝试使用 std::chrono::high_resolution_clock。但是,如果 high_resolution_clock 的精度仍然不够,你可以尝试使用硬件提供的时钟源,并将其封装成一个自定义时钟。

时间点操作:加减,比较,转换

有了时钟,我们就可以获取时间点了。接下来,我们需要对时间点进行各种操作,比如加减,比较,转换等。

  • 加减: 可以使用 +- 运算符来对时间点进行加减操作。
  • 比较: 可以使用 ==, !=, <, >, <=, >= 运算符来比较时间点。
  • 转换: 可以使用 std::chrono::time_point_cast 来将时间点转换为不同的精度。
#include <iostream>
#include <chrono>

int main() {
    auto now = std::chrono::system_clock::now();

    // 加法
    auto later = now + std::chrono::seconds(10);
    std::cout << "10 seconds later: " << std::chrono::system_clock::to_time_t(later) << std::endl;

    // 减法
    auto earlier = now - std::chrono::minutes(5);
    std::cout << "5 minutes earlier: " << std::chrono::system_clock::to_time_t(earlier) << std::endl;

    // 比较
    if (later > now) {
        std::cout << "Later is indeed later than now." << std::endl;
    }

    // 转换精度
    auto now_ms = std::chrono::time_point_cast<std::chrono::milliseconds>(now);
    std::cout << "Current time in milliseconds: " << now_ms.time_since_epoch().count() << std::endl;

    return 0;
}

时间点与 time_t 的转换

std::chrono::system_clock 的时间点可以与 time_t 类型进行转换,time_t 是 C 语言中表示时间的类型。

  • std::chrono::system_clock::to_time_t(time_point):system_clock 的时间点转换为 time_t
  • std::chrono::system_clock::from_time_t(time_t):time_t 转换为 system_clock 的时间点。
#include <iostream>
#include <chrono>
#include <ctime>

int main() {
    auto now = std::chrono::system_clock::now();
    std::time_t now_c = std::chrono::system_clock::to_time_t(now);
    std::cout << "Current time as time_t: " << now_c << std::endl;
    std::cout << "Current time (ctime): " << std::ctime(&now_c) << std::endl; // 使用 ctime 转换为可读字符串

    auto tp = std::chrono::system_clock::from_time_t(now_c);
    std::cout << "Time point from time_t: " << std::chrono::system_clock::to_time_t(tp) << std::endl;

    return 0;
}

高级技巧:使用 std::ratio 定义非标准时间单位

std::ratio 允许我们定义任意的时间单位,而不仅仅是秒,毫秒,微秒等。例如,我们可以定义一个“赫兹”作为时间单位:

#include <iostream>
#include <chrono>

int main() {
    using Hertz = std::ratio<1, 1>; // 1 赫兹 = 1 秒
    using KiloHertz = std::ratio<1000, 1>; // 1 千赫兹 = 1000 秒
    using duration_khz = std::chrono::duration<long long, KiloHertz>;

    duration_khz d(5); // 5 千赫兹
    std::cout << "5 KHz is " << d.count() << " ticks" << std::endl;
    std::cout << "5 KHz in seconds is " << std::chrono::duration_cast<std::chrono::seconds>(d).count() << " seconds" << std::endl;

    return 0;
}

自定义时钟的实际应用场景

  • 游戏开发: 可以使用自定义时钟来控制游戏中的时间流逝,例如,可以创建一个“游戏时钟”,让游戏中的时间比现实时间快或慢。
  • 嵌入式系统: 可以使用自定义时钟来访问硬件提供的时钟源,例如,可以创建一个“硬件时钟”,直接读取硬件寄存器中的时间值。
  • 分布式系统: 可以使用自定义时钟来实现逻辑时钟,例如,可以使用 Lamport 时钟或向量时钟来保证事件的顺序。
  • 性能分析: 可以使用自定义时钟来测量代码的执行时间,例如,可以创建一个“性能时钟”,只测量 CPU 时间或 GPU 时间。

线程安全问题

当多个线程访问同一个自定义时钟时,需要考虑线程安全问题。如果时钟的状态是可变的(例如,模拟时钟),则需要使用互斥锁或其他同步机制来保护时钟的状态。

#include <iostream>
#include <chrono>
#include <thread>
#include <mutex>

class ThreadSafeClock {
public:
    using rep = long long;
    using period = std::ratio<1>;
    using duration = std::chrono::duration<rep, period>;
    using time_point = std::chrono::time_point<ThreadSafeClock>;
    static constexpr bool is_steady = true;

    static void set_now(time_point tp) {
        std::lock_guard<std::mutex> lock(mutex_);
        current_time_ = tp;
    }

    static time_point now() noexcept {
        std::lock_guard<std::mutex> lock(mutex_);
        return current_time_;
    }

private:
    static time_point current_time_;
    static std::mutex mutex_;
};

ThreadSafeClock::time_point ThreadSafeClock::current_time_ = ThreadSafeClock::time_point(ThreadSafeClock::duration(0));
std::mutex ThreadSafeClock::mutex_;

int main() {
    std::thread t1([]() {
        for (int i = 0; i < 10; ++i) {
            ThreadSafeClock::set_now(ThreadSafeClock::time_point(ThreadSafeClock::duration(i)));
            std::cout << "Thread 1: " << ThreadSafeClock::now().time_since_epoch().count() << std::endl;
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
    });

    std::thread t2([]() {
        for (int i = 10; i < 20; ++i) {
            ThreadSafeClock::set_now(ThreadSafeClock::time_point(ThreadSafeClock::duration(i)));
            std::cout << "Thread 2: " << ThreadSafeClock::now().time_since_epoch().count() << std::endl;
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
    });

    t1.join();
    t2.join();

    return 0;
}

在这个例子中,我们使用 std::mutex 来保护 current_time_ 的访问,确保线程安全。

总结

今天我们一起探索了 C++ std::chrono 库中自定义时钟和时间点操作的高级用法。我们学习了如何创建自定义时钟,包括简单的墙上时钟、模拟时钟和线程安全时钟。我们还学习了如何对时间点进行加减,比较,转换等操作。希望这些知识能帮助你在实际项目中更好地使用 std::chrono 库。

记住,时间就是金钱,哦不,时间就是代码! 掌握时间处理,你就能更好地掌控程序的命运。

感谢大家的收看,我们下期再见! 别忘了点赞,关注,转发! (虽然这里不能点赞…)

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注