定时器概述

这篇具有很好参考价值的文章主要介绍了定时器概述。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

定时器详解

引出

定时器是一个比较常见的数据结构,或者说框架,以一个最简单的例子引出,在游戏中,冷却时间使用的就是定时器;

所以说定时器是等待时间过期执行对应时间事件处理( 回调函数 )的一个框架;

补充:下文中可能会出现定时任务,它和时间事件基本上是一个东西

那么现在有一个就有一个问题,该如何实现定时器这一功能?

  • 首先进行两种分类:随着网络事件处理定时事件;不随着网络事件处理时间事件;

定时器概述

对于一个服务器来说,需要许多客户端进行通信,必然会有网络IO相关的事件( 网络IO事件 );

除此之外,服务器内部对于一个或N个客户端传输过来的数据进行延时相关的处理,针对不同送达时间,必然会有排序和时间事件

对于不同的框架,针对网络事件和时间会有不同的实现:

  • 第一种,网络事件和定时事件在同一个线程中使用;
  • 第二种:网络事件和定时事件在不同的线程中使用;

第一种形式-网络事件和定时事件在同一个线程中使用

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, 
           struct timeval *timeout);
timeout > 0:表示等待指定的时间,单位为毫秒;
timeout == 0:表示立即返回,不管是否有事件发生;
timeout == -1:表示一直等待,直到有事件发生。
  • 这一模式中,利用的是IO多路复用中的timeout参数;使用其大于0与等于0的情况,以一下代码举例。
// ... 其他代码
while(!quit){
    
	int timeout=get_nearest_timer()-now_time();// get_nearest_timer为得到最近的时间事件
	if(timeout<0)
		timeout=-1;
	int event_count=epoll_wait(epfd,ev,ev_num,timeout);
	for(int i=0;i<event_count;i++){
        
		// ... 处理网络事件
	}
    
	update_timer();// 处理时间事件
	// ... 其他代码
}
// ... 其他代码

说明:get_nearest_timer()、update_timer(),为定时器提供

  • 从上述中可知其流程:
    • 最近定时任务需要的时间->epoll_wait阻塞一会儿->处理网络事件->处理时间事件;
  • update_timer()函数可以只是用作确定时间事件的回调函数,可以把事件处理抛给线程池,而update_timer()会直接进行返回,不会阻塞主流程,这是一种事件的异步形式。

总结

针对于上述流程,最关键的一步就是使用epoll_wait进行阻塞,使得可以同步地进行网络IO处理和定时任务处理,让其处于一个流程中

当然这种必然也是有缺点的,比如定时任务或者网络IO数量太多,导致处理时间太长,不过这可以使用 reactor模型( 或者使用协程)+时间事件异步处理 的形式解决。多数情况下还是针对少量定时任务使用第一种。

再有就是多线程环境下,如果网络IO的事件处理和时间任务处理需要有一种同步的关系,大概还得进行加锁的复杂处理

扩展

  • 其实reactor模型也是异步事件处理,从而让IO处理时同步的

第二种:网络事件和定时事件在不同的线程中使用

概况:时间事件使用一个单独的线程进行检测,一旦检测到进行回调、或者再抛给任务队列,由线程池进行处理;

void* thread_timer(void * thread_param) {
    init_timer();// 初始化定时器
    while (!quit) {
        update_timer(); // 检测定时器中的定时任务
        sleep(t); // 这⾥的 t 要⼩于 时间精度
    }
    clear_timer();
    return NULL;
}
pthread_create(&pid, NULL, thread_timer, &thread_param);

说明:

  • 定时器内部会维护大量的定时任务,update_timer()是用于检测定时器中所有需要被处理的定时任务,检测到后进行处理并删除该定时任务,处理时可以直接调用回调,也可以做成异步的事件处理。

  • 使用sleep()/usleep()函数,减少进入update_timer()函数的次数,不过一定要让t小于时间精度(最小运行单元),否则t大于时间精度会让定时器不会及时的被检测到,会出现延时的状况。

总结

在不同线程处理网络事件和时间事件时,可以进行大量的定时任务的维护和处理,让网络IO处理和定时任务解耦。

定时器设计

既然要使用定时器,必然离不开定时器的设计,那么如何设计定时器,成为接下来需要解决的问题。

接口设计

首先是接口的设计

基本接口:

  1. 初始化定时器:void init_timer();
  2. 添加定时任务:Node *add_timer(int expire,callback cb);
  3. 删除定时任务:bool delete_timer(Node* node);一般在检测定时器的时候使用,可以不对外提供该接口;
  4. 找到最近的需要被触发的定时任务:Node* find_nearest_timer();这种接口一般应用于网络事件和定时事件在一个线程中处理的触发方式。
  5. 检测更新所有定时任务:void update_timer();
  6. 清除所有定时任务 :void clear_timer();

内部数据结构选择

对外提供的接口解决了,那么接下来该思考定时器内部该用什么样的数据结构维护定时任务会让效率达到最佳;

该数据结构需要满足一些条件,比如查询最小节点的时间复杂度比如得小,比如增加删除节点不会影响原有的数据结构。

因此选择出来了,红黑树、最小堆都是最佳的选择。

红黑树

红黑树的中序遍历,让数据可以有序排序,而且是绝对排序(从小到大排序)。具体数据结构,可以去查找网上其他资料。

  • 增删查的时间复杂度均为O(logN);
  • 最小的节点为红黑树最左侧的节点,时间复杂度O(logN);
  • 如果几乎同时来个两个时间相等的任务,但是key值比较时会出现相等的情况,你可以把后来的任务放到其右节点位置。

最小堆

最小堆是一个完全二叉树,且父节点一定比子节点小,这样得到的结果就是根节点一定是这棵树的最小值;具体数据结构,可以去查找网上其他资料。

  • 增查的时间复杂度O(logN);

  • 删除操作需要查找是否包含这个节点,删除的时间复杂度O(n);

  • 最小节点的时间复杂度为O(1);

时间轮

以上两种实现方法需要时时刻刻都要进行检测,对于定时任务密集类型的自然可以,如果是定时任务之间的间隔时间过长怎么办?时时刻刻检测会增加很多无效检测,降低cpu的使用效率。

使用时间轮是一种解决上述问题的方法;

时间轮需要保证自己的时间精度足够小,否则会出现不能插入定时任务的情况

单层级时间轮

首先先谈一谈单层级时间轮,单层级时间轮就是使用数组模拟一个环形队列,数组的每一个元素是一个定时任务链表。通过一个指针不断遍历这个数组,遇到某个元素的任务链表不为空就执行相应的任务。

对于单层级时间轮需要数组的大小足够大,否则插入的定时任务超出数组可关注时间(比如时间精度1ms,数组大小为20,可关注的时间就是20ms)的N多倍,会导致定时任务执行顺序出错。(不确定在定时任务的结构体增加一个圈数可不可以);

多层级时间轮

那么多层级时间轮就是解决上述问题一个好的解决办法。类似于进制进位一样,一旦超过该层级的可关注时间,就会进入下一层级,直到有位置可以插入;如果某个定时任务即将执行,也会不断返回上一层级,直至挂载到第一层级(也是被执行层级)。

如果使用时间轮也是直接使用多层级时间轮的,它有以下这些优点:

  • 相比于单层级时间轮,它的空间占用大大减少;因为多层级针对任务的时间储存是乘的关系,而单层级是加的关系
  • 对于多层级,除去第一层的任务需要时时刻刻关注,其他层的元素每增加一次重新映射一次即可。

数据结构

struct timer_event {
	uint32_t handle;
	int session;
};
// 定时任务
struct timer_node {
	struct timer_node *next;
	uint32_t expire;
};
// 任务链表
struct link_list {
	struct timer_node head;
	struct timer_node *tail;
};
struct timer {
	struct link_list near[TIME_NEAR];
	struct link_list t[4][TIME_LEVEL];
	struct spinlock lock;	// 自旋锁,多线程操作需要,时间复杂度O(1)
	uint32_t time;			//时间指针,范围是2^32*10ms
	uint32_t starttime;
	uint64_t current;
	uint64_t current_point;
};

因此针对时间轮设计,需要确定几点:

  1. 确定时间范围。每一层级允许的添加任务的时间范围
  2. 确定时间精度。每一次tick(指针增加一次)的时间
  3. 确定时间层级。第一层组织最近关注的延时任务,是实际执行的层级;其他层级只负责向上一层级重新映射。
  4. 实现添加任务接口。
  5. 实现删除任务的接口。对于删除任务,一般不采取直接从链表删除的方式,因为时间指针一直再增加,定时任务可能会被重新映射,节点位置发生改变。因此,可以通过添加一个标记字段cancel,当cancel=true时不执行具体任务。
  6. 实现重新映射,每一次时间指针移动都需要判断是否可重新映射。
时间轮总结

时间轮顾名思义,是仿照时钟规律而发明出来的。使用时间轮就是使用多层级时间轮。

时间轮通过多个层级存放定时任务,第一层组织最近关注的延时任务,是实际执行的层级;其他层级只负责向上一层级重新映射。

对于相同时间触发的定时任务,时间轮采用一个链表将它们存放在同一个时间槽,时间到达时一起执行。

多线程下,定时器只管检测,检测到了定时任务,将其抛给线程池执行,定时器本身线程不执行定时任务的业务逻辑。

时间轮删除节点很不方便,一般不采用删除方式,因为时间指针一直在移动,定时任务可能会被重新映射,节点位置发生改变;因此,可以通过添加一个标记字段cancel,当cancel=true时不执行具体任务。

内部数据结构选择总结

多线程环境下,使用最小堆和红黑树的定时器,在进行添加任务、删除任务的时候需要把整个树进行加锁,使用互斥锁;如果是时间轮,由于其时间复杂度为O(1),对于添加任务、删除任务使用自旋锁即可。

如果定时任务比较少,可以选择红黑树或者最小堆;如果定时任务比较多,选择时间轮。

说明

写这篇文章的目的,是捋清定时器会有哪些问题,从宏观的一个角度去看待定时器,如果内容有哪些地方不够细节,可以利用谷歌、必应进行搜索,深度了解其原理、或者实现代码。比如红黑树的数据结构是什么样的、最小堆的数据结构是什么样的等这些问题我相信有很多大牛会比我讲的更好,或者说有着更深刻的理解。

此外,本人目前只是一名大学生,对于编程自认为没有达到很高的水平,而且很多地方也有一些借鉴的成分,如果文章中有任何不对的地方,欢迎指正,本人会虚心接受的。文章来源地址https://www.toymoban.com/news/detail-444251.html

到了这里,关于定时器概述的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【江科大】STM32:(超级详细)定时器输出比较

    输出比较单元 名词解释 IC input capture 输入捕获 OC outpucompare 输出比较 CC capture compare 输入捕获和输出比较的单元 OC(Output Compare)输出比较 :用来输出PWM波形 特点 输出比较可以通过比较CNT(计数器)与CCR(捕获/比较)寄存器值的关系,来对输出电平进行置1、置0或翻转的操作

    2024年01月23日
    浏览(47)
  • 【STM32学习】——定时器输出比较功能&PWM脉宽调制&通用/高级定时器输出比较通道&舵机/直流电机简介&PWM驱动呼吸灯/舵机/直流电机代码实操

    声明:学习笔记根据b站江科大自化协stm32入门教程编辑,仅供学习交流使用!

    2024年02月03日
    浏览(58)
  • STM32笔记——定时器输出比较功能(产生PWM波)

    目录 一、概述 二、PWM简单介绍  三、通用定时器输出比较 3.1 输出比较简介 3.2 输出比较通道 3.3 产生PWM的过程 四、实验硬件介绍及PWM模块程序 4.1 舵机简介 4.2 直流电机及驱动芯片TB6612  4.3 PWM模块驱动程序         主要介绍通用定时器输出比较功能,在GPIO口输出PWM来控

    2024年02月13日
    浏览(48)
  • STM32CubeMX教程8 TIM 通用定时器 - 输出比较

    开发板(STM32F407G-DISC1) STM32CubeMX软件(Version 6.10.0) keil µVision5 IDE(MDK-Arm) ST-LINK/V2驱动 逻辑分析仪nanoDLA 使用STM32CubeMX软件配置STM32F407 通用定时器的输出比较通道 ,并将其输出到四个LED灯引脚实现LED灯流水灯效果 STM32F407的定时器通道均可以实现输出比较功能, 输出比较功

    2024年02月03日
    浏览(80)
  • HAL库STM32常用外设教程(五)—— 定时器 输出比较

    有关于定时器 输出PWM功能 不了解的可以看这篇文章 :HAL库STM32常用外设教程(一)—— 定时器 输出PWM 有关于定时器 定时功能 不了解的可以看这篇文章 :HAL库STM32常用外设教程(四)—— 定时器 基本定时 1、STM32F407ZGT6 2、STM32CubeMx软件 3、keil5 内容简述: 通篇文章将涉及以

    2024年03月27日
    浏览(64)
  • STM32单片机(六)TIM定时器 -> 第三节:TIM输出比较

    ❤️ 专栏简介:本专栏记录了从零学习单片机的过程,其中包括51单片机和STM32单片机两部分;建议先学习51单片机,其是STM32等高级单片机的基础;这样再学习STM32时才能融会贯通。 ☀️ 专栏适用人群 :适用于想要从零基础开始学习入门单片机,且有一定C语言基础的的童鞋

    2024年02月09日
    浏览(65)
  • STM32定时器-定时器中断功能详解

    STM32的众多定时器中我们使用最多的是高级定时器和通用定时器,而高级定时器一般也是用作通用定时器的功能,下面我们就以通用定时器为例进行讲解,其功能和特点包括: 通用与基本定时器(2~7)位于低速的APB1总线上 高级定时器(1、8)位于高速的APB2总线上 自动装载计

    2024年02月08日
    浏览(56)
  • STM32 MCU 定时器详解(3)--高级定时器

    16位递增、递减、中心对齐计数器(计数值:0~65535) 16位预分频器(分频系数:1~65536) 可用于触发DAC、ADC 在更新事件、触发事件、输入捕获、输出比较时,会产生中断/DMA请求 4个独立通道,可用于:输入捕获、输出比较、输出PWM、单脉冲模式 使用外部信号控制定时器且可实

    2024年04月17日
    浏览(49)
  • 定时器详解 -- 定时器中断、PWM输出 --stm32

    STM32F103系列芯片拥有多种定时器,包括基本定时器、通用定时器和高级定时器,每种定时器都具有一些特定的功能。 向上计数:计数器从0计数到自动重装载值(ARR),然后重新从0开始计数并且产生一个计数器溢出事件。 向下计数:计数器从自动重装载值(ARR)开始向下计数

    2024年02月11日
    浏览(63)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包