C++ 手写常见的任务定时器

news2024/11/25 14:53:04

序言

 最近在编写 C++ 的服务器代码时,我遇到了一个需求,服务器很可能会遇到那些长期不活跃的连接,这些连接占用了一定的资源但是并没有进行有效的通信。为了优化资源使用,我决定实现一个定时器,以便定期检查连接的活跃状态并适时关闭那些不再活跃的连接。
 在这里我将介绍以下三种方式:
在这里插入图片描述


1. 循环遍历任务定时器

1.1 原理

 这个的原理就相当简单了,我们利用一块连续的空间来存储我们需要执行的任务以及指定执行的时刻,每隔一段时间来判断哪些任务就绪了,我们就执行该任务。具体流程图如下:
在这里插入图片描述

 在这里有几个点需要注意了,需求不同,设计也不同:

  • 执行后是否移除:当我们执行我们的定时任务后,是否需要将他从我们的任务队列移除呢,这个完全就取决于自己的需求了,就比如我的需求是关闭某个连接,那么执行该任务后连接也没了,自然是要移除该任务的
  • 休眠时间:这个就要看你的要求是否严格了,如果需要较小的时间差异,那么就将休眠时间间隔短一些,但是 CPU 的负担也大。

1.2 实现

 首先我们需要一块空间来存储我们的任务并且方便遍历,那么就首选 vector
了,我们还需要设计在容器中元素的类型,该类型的任务是表示一个定时任务:

using TaskFunc = std::function<void()>;
using Timer    = std::chrono::steady_clock::time_point;

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

之后我们定时器无非就是添加任务,启动函数:

// 添加定时任务
void AddTask(TaskFunc callback, int timeout) 
{
    auto execute_time = std::chrono::steady_clock::now() + std::chrono::seconds(timeout); // 计算执行时间
    _tasks.push_back({ execute_time, callback });
}

// 运行定时器
void Loop() 
{
    while (true) 
    {
        auto now = std::chrono::steady_clock::now();
        for (auto it = _tasks.begin(); it != _tasks.end(); ++it)
        {
            if (now >= it->_execute_time)
            {
                it->_call_back();
                it = _tasks.erase(it);
            }
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

 实现起来还是比较简单的,但是在需要注意,在执行的时候一般是使用一个专门的线程来进行定时器的运行,这可能会涉及到线程安全的问题,所以我们需要进一步完善的话,需要进行加锁操作。

1.3 优缺点

 优点很明显:

  • 简单易用:循环遍历定时器通常简单直观,易于实现和理解。
    是的就没了,我也没想出其他的优点,欢迎大家在评论区提出。

 缺点的话也很明显:

  • 资源消耗:如果循环遍历频率过高,可能会导致 CPU 资源浪费,尤其是在高负载情况下。
  • 效率问题:当定时器数量很大时,每次遍历所有定时器会变得低效,因为即使大部分定时器尚未超时,也需要被检查。

作为一个启蒙的任务定时器还不错的,但是实际使用的话还是算了吧…


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 一样,不需要频繁的扩容操作

 缺点:

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

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


3. 时间轮定时器

1.1 原理

 大家对秒钟肯定都比较的熟悉吧,秒钟的单位是 1s,每隔一秒就走一步。现在,我们也创建一个类似于秒钟的功能的数据结构:
在这里插入图片描述

 一共有八个格数,如果我们依旧使用秒为单位的话,那么最多可以表示 8s。这和我们今天的任务定时器的关系是什么呢?我们将该计时任务存储在现在 (秒针 + 任务时间)% 最大刻度 的位置上,并且秒针指向哪个位置,哪个位置的任务就执行!

 没听懂,没关系,我第一次也没听懂,那我们模拟一遍,相信会好很多:

  • 不越界的情况:现在秒针指向位置 1,我想要添加一个时间为 2s 的任务,那么该该任务存储在 (1 + 2)% 8 = 3 的位置。时间过 1s ,秒针指向 2,执行 2 中的全部任务(现在为空,就不执行)!时间再过 1s,秒针指向 3,执行 3 中的全部任务。OK!我们的目标达成了!
  • 越界的情况:现在秒针指向位置 3,我想要添加一个时间为 7s 的任务,那么该该任务存储在 (3 + 7)% 8 = 2 的位置。情况就和第一种一样了,所以 %运算 保证了我们的任务存储在恰当的运算!

这就是时间轮!现在基本怎么运行大家心里或多或少了解了一点,我提出一些疑问,也可能会是你的疑问:

  • 如果我有多个定时任务需要存储在 2 的位置,怎么办?
  • 你的刻度最大 8s,我想要一个 10s 的任务,怎么办?

问题一:
 在我们的哈希表中如果产生了哈希冲突,我们采用的一个方式为 链地址法。(新增的话,使用链表更为高效,不用进行扩容操作)我们也可以呀:
在这里插入图片描述

问题二:
 所以我们在创建时间轮时我们需要确定一个合适的时间范围大小。但是如果我想要一个 1000000s 大小的时间轮不可能申请这么大个空间吧?当然不是,大家可以去了解一下多级时间轮。

1.2 实现

 首先我们需要考虑什么数据结构是环状的呀,好像并没有接触过。其实我们使用 vector 就可以啦!啊?后者不是线性的一段空间吗,怎么会是环状的呢?没关系,我们再遍历数组的时候使用 %运算 不就好了嘛。我演示一下:

int main() 
{
	std::vector<int> array = { 1,2,3,4,5 };
	int index = 0;
	while (true)
	{
		std::cout << array[index] << " ";
		index = (index + 1) % array.size(); // 这里是关键,每当遍历完时,重头再来

		std::this_thread::sleep_for(std::chrono::seconds(1));
	}

	return 0;
}

输出结果:

1 2 3 4 5 1 2 3 4 5 1 …

所以说经过 % 他的逻辑结构已经成环了!

 之后我们需要了解时间轮的成员变量应该需要哪些:

using TaskFunc = std::function<void()>;
using Wheel = std::vector<std::list<TaskFunc>>;

int   _tick;     // 秒针
int   _capacity; // 最大容量(刻度)
Wheel _wheel;    // 时间轮 

有了这些之后,我们需要指定构建函数,构建函数中需要指定时间轮的最大刻度:

TimeWheel(int capacity) 
    : _tick(0), _capacity(capacity), _wheel(_capacity)
{}

最后也就是我们的新增函数以及启动函数:

void AddTask(TaskFunc callback, int timeout) 
{
    int index = (_tick + timeout) % _capacity;
    _wheel[index].push_back(callback);
}

void Loop() 
{
    while (true) 
    {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        _tick = (_tick + 1) % _capacity;    // 秒针移动
        
        // 执行该位置的函数
        for (auto& func : _wheel[_tick])
        {
            func();
        }
        // 清除
        _wheel[_tick].clear();
    }
}

在这里我们尤其需要注意不要越界了!

1.3 优缺点

 优点:

  • 高效性:时间轮能够以 O(1) 的复杂度处理定时任务的插入和删除,适合大量定时任务的场景。
  • 节省空间:通过使用固定大小的轮子和槽,时间轮在内存使用上较为高效,避免了过多的动态内存分配。

 缺点:

  • 时间精度限制:时间轮的精度由槽的大小决定,若需要高精度的定时任务,可能不适合使用时间轮。

 到现在为止,时间轮是我见过最精妙的一种方式,编程之美呀!


4. 总结

 今天的内容简单地向大家介绍了三种任务定时器方式,遍历方式最为简单,效率也比较低,小堆方式处理大数据集有优势,数据量少了消耗反而还多了,最后一种时间轮是比较全面的一种,但是要确定合适的时间范围。希望大家有所收获!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2232394.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

后端Java学习:springboot之文件上传(阿里云OSS存储)

一、什么是阿里云存储&#xff1f; 阿里云对象存储OSS&#xff08;Object Storage Service&#xff09;&#xff0c;是一款海量、安全、低成本、高可靠的云存储服务。使用OSS&#xff0c;您可以通过网络随时存储和调用包括文本、图片、音频和视频等在内的各种文件。 二、阿里云…

2024年前三季度币安、OKX等五大交易所上币表现分析

随着加密市场竞争的加剧&#xff0c;头部交易所逐渐在上币策略、代币选择、交易活跃度等方面采取了不同的应对策略。Animoca Digital Research近期发布的一份报告&#xff0c;通过对币安、OKX、Bitget、KuCoin和Bybit五大交易所2024年前三季度的上币情况进行了详细分析。本文将…

嵌入式linux系统中串口驱动框架分析

大家好,今天主要给大家分享一下,如何使用linux系统中的串口实现。 第一:串口基本简介 串口是很常见的一个外设,在Linux下通常通过串口和其他设备或传感器进行通信。根据电平的不同,串口可以分为TTL和RS232。不管是什么样的电平接口,驱动程序是一样的。 第二:Linux下UAR…

服务器内存不够导致postgresql进程被kill的问题记录

服务器环境&#xff1a;Centos7.9&#xff0c;PGSQL14 故障现象 平均负载飙升至80以上 磁盘 IO 高: 故障期间磁盘 IO 明显增加 同步异常: 主从库的复制出现问题&#xff0c;从库自动提升为主库 排查过程 磁盘 IO&#xff1a;使用 iostat查看磁盘 IO 活动&#xff0c;发现磁盘…

解决方案:e1000e eno1 Detected Hardware Unit Hang

在 Proxmox 6.5.11-8 中&#xff0c;偶发性会出现以下报错&#xff0c;尤其是在进行大文件传输后&#xff1a; [97377.240263] e1000e 0000:00:1f eno1: Detected Hardware Unit Hang:TDH <22>TDT <2f>next_to_use &l…

Nature文章《deep learning》文章翻译

这篇文章是对Nature上《deep learning》文章的翻译。原作者 Yann LeCun, Yoshua Bengio& Geoffrey Hinton。 这篇文章的中心思想是深入探讨深度学习在机器学习中的革命性贡献&#xff0c;重点介绍其在特征学习、监督学习、无监督学习等方面的突破&#xff0c;并阐述其在图…

微服务实战系列之玩转Docker(十六)

导览 前言Q&#xff1a;基于容器云如何实现高可用的配置中心一、etcd入门1. 简介2. 特点 二、etcd实践1. 安装etcd镜像2. 创建etcd集群2.1 etcd-node12.2 etcd-node22.3 etcd-node3 3. 启动etcd集群 结语系列回顾 前言 Docker&#xff0c;一个宠儿&#xff0c;一个云原生领域的…

Rust 力扣 - 1423. 可获得的最大点数

文章目录 题目描述题解思路题解代码题目链接 题目描述 题解思路 题目所求结果存在下述等式 可获得的最大点数 所有卡牌的点数之和 - 长度为&#xff08;卡牌数量 - k&#xff09;的窗口的点数之和的最小值 我们遍历长度为&#xff08;卡牌数量 - k&#xff09;的窗口&#…

flink 内存配置(二):设置TaskManager内存

TaskManager在Flink中运行用户代码。根据需要配置内存使用&#xff0c;可以极大地减少Flink的资源占用&#xff0c;提高作业的稳定性。 注意下面的讲解适用于TaskManager 1.10之后的版本。与JobManager进程的内存模型相比&#xff0c;TaskManager内存组件具有类似但更复杂的结构…

配置DDNS结合光猫路由器实现外网映射

配置ddns结合光猫路由器实现外网映射 一、实现思路 首先需要去获取一个动态域名&#xff08;文章不再赘述&#xff0c;重点去介绍具体实现&#xff09;&#xff0c;用作后面与与公网绑定。然后需要在光猫和路由器上去做配置&#xff0c;同时确保路由器有公网IP&#xff0c;最…

如何在BSV区块链上实现可验证AI

​​发表时间&#xff1a;2024年10月2日 nChain的顶尖专家们已经找到并成功测试了一种方法&#xff1a;通过区块链技术来验证AI&#xff08;人工智能&#xff09;系统的输出结果。这种方法可以确保AI模型既按照规范运行&#xff0c;避免严重错误&#xff0c;遵守诸如公平、透明…

华为HarmonyOS打造开放、合规的广告生态 - 激励广告

场景介绍 激励广告是一种全屏幕的视频广告&#xff0c;用户可以选择点击观看&#xff0c;以换取相应奖励。 接口说明 接口名 描述 loadAd(adParam: AdRequestParams, adOptions: AdOptions, listener: AdLoadListener): void 请求单广告位广告&#xff0c;通过AdRequestPar…

easyui +vue v-slot 注意事项

https://www.jeasyui.com/demo-vue/main/index.php?pluginDataGrid&themematerial-teal&dirltr&pitemCheckBox%20Selection&sortasc 接口说明 <template><div><h2>Checkbox Selection</h2><DataGrid :data"data" style&…

unity搭建场景学习

unity搭建场景学习 创建场景创建gameobject创建材质&#xff0c;用于给gameobject上色拖拽材质球上色上色原理设置多个材质方式设置贴图的方式 效果设置光滑度一些预览设置菜单渲染模型与碰撞模型网格渲染参数1. materials(材质)2. lighting(光照)3. reflection probes(反射探针…

软件加密与授权管理:构建安全高效的软件使用体系

“软件加密与授权管理&#xff1a;构建安全高效的软件使用体系”是一个全面且深入的议题&#xff0c;以下是对该议题的详细探讨&#xff1a; 一、软件加密的概念与重要性 软件加密是指为软件添加保护措施&#xff0c;以防止其被盗版或非法复制。这一技术站在软件开发者的角度&a…

【VScode】中文版ChatGPT编程工具-CodeMoss!教程+示例+快捷键

文章目录 1. 多模型选择2. 编辑快捷键3. 历史记录收藏 CodeMoss使用教程1. 安装CodeMoss插件2. 配置AI模型3. 使用快捷键4. 进行代码优化与解释5. 收藏历史记录 总结与展望 在当今快速发展的编程世界中&#xff0c;开发者们面临着越来越多的挑战。如何提高编程效率&#xff0c;…

宝塔Linux面板安装PHP扩展失败报wget: unable to resolve host address ‘download.bt.cn’

一、问题&#xff1a; 当使用宝塔面板安装PHP扩展失败出现如下错误时 Resolving download.bt.cn(download.bt.cn)...failed: Connection timed out. wget: unable toresolve host address download.bt.cn’ 二、解决&#xff1a; 第一步&#xff1a;如下命令执行拿到返回的I…

Scrapy源码解析:DownloadHandlers设计与解析

1、源码解析 代码路径&#xff1a;scrapy/core/downloader/__init__.py 详细代码解析&#xff0c;请看代码注释 """Download handlers for different schemes"""import logging from typing import TYPE_CHECKING, Any, Callable, Dict, Gener…

【C++】对左值引用右值引用的深入理解(右值引用与移动语义)

&#x1f308; 个人主页&#xff1a;谁在夜里看海. &#x1f525; 个人专栏&#xff1a;《C系列》《Linux系列》 ⛰️ 天高地阔&#xff0c;欲往观之。 ​ 目录 前言&#xff1a;对引用的底层理解 一、左值与右值 提问&#xff1a;左值在左&#xff0c;右值在右&#xff1f;…

docker下迁移elasticsearch的问题与解决方案

欢迎来到我的博客&#xff0c;代码的世界里&#xff0c;每一行都是一个故事 &#x1f38f;&#xff1a;你只管努力&#xff0c;剩下的交给时间 &#x1f3e0; &#xff1a;小破站 docker下迁移elasticsearch的问题与解决方案 数据挂载报错解决权限问题节点故障 直接上图&#x…