C++ 手写常见的任务定时器-2. 小堆任务定时器

1.1 原理

 首先我们先回忆一下小堆这种数据结构:
在这里插入图片描述

 在小堆中,树的每个节点的值都小于或等于其子节点的值。这意味着堆的最小元素总是位于树的根部,即堆顶。 当我们取出该堆的最小元素时,步骤是:

  1. 交换堆顶和末尾元素:将堆顶元素(最小元素)与堆的最后一个元素交换位置。此时,堆顶元素不再是最小元素
  2. 调整堆:从堆顶开始,将交换后的堆顶元素与它的子节点比较,如果它大于其子节点中的任何一个,则与较小的子节点交换位置,并继续这一过程直到堆顶元素小于其所有子节点或到达叶节点。
  3. 移除最小元素:由于最小元素已经与堆的最后一个元素交换,现在可以直接从数组中移除它

 这和我们说的任务定时器啥关系呢?现在,我们堆的每一个元素就不再是一个数了,而是我们的 TimerTask,我们将所有任务构造为一个小堆,之后任务检测步骤如下:

  1. 检查堆顶元素:检查堆顶的任务(时间最小的任务)执行时间是否到了
    • 条件不满足: 直接休眠指定时间再检查,时间最小的都不行,其他的更不行了
    • 条件满足:取下堆顶元素执行任务,调整堆的结构使其为小堆
  2. 重复 1 - 2 的步骤

这样的话我们就不需要每次都遍历整个数组,具体的优缺后面说。

1.2 实现

 在这里我们存储人物的容器就换成 priority_queue 了,因为需要实现一个小堆。其次任务结构体,我们需要在原来的基础上加上一个比较函数,因为涉及到自定义结构体排序比较的操作:

struct TimerTask
{
    TimerTask(Timer time, TaskFunc callback)
        : _execute_time(time), _call_back(callback)
    {}
	
	// 比较函数但是底层我们写为 > , 因为我们需要得到一个小堆
    bool operator<(const TimerTask& other) const 
    {
        return _execute_time > other._execute_time;
    }

    Timer    _execute_time; // 定时器
    TaskFunc _call_back;    // 回调函数
};

之后稍有一些不一样的地方就是启动函数:

void Loop() 
{
    while (true) 
    {
        auto now = std::chrono::steady_clock::now();
        while (! _tasks.empty() && _tasks.top()._execute_time <= now) 
        {
            auto task = _tasks.top();
            _tasks.pop();
            task._call_back();
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

 实现起来不难,主要是思想很重要。所以说熟悉数据结构是很重要一件事!

1.3 优缺点

 网上都说小堆的性能是好于我们第一种遍历方案的,但是我们要思考高效在哪里呢?我们算一笔账,在 n 个任务中 m 个任务就绪了, 前者执行定时任务的时间复杂度是 O(n),因为是遍历嘛;后者的话涉及到堆的调整操作 O(logn),一共取 m,那就是 O(mlogn)
 当我们的数据量也就是 n 较小的情况下,请问谁更好一点呢?我想,是前者吧。(化学上有一句话是,抛开计量谈毒性,就是耍流氓!咋们计算机也差不多,抛开数据量谈高效,也是耍流氓!)
 优点:

  • 高效的任务管理:当数据量较大时,性能更加的不错。
  • 底部开销:不和 vector 一样,不需要频繁的扩容操作

 缺点:

  • 不适合小任务集:当任务数量很少时,使用小堆可能反而不如简单的线性结构(如数组或链表)高效,因为堆的维护开销可能超过直接遍历的成本。

所以我们还是要带着辩证的角度看待问题!


上一篇:【Linux】安装并配置 Microsoft SQL Server 数据库(Ubuntu 22.04)-安装步骤


下一篇:qt QStackedLayout详解-重要方法