时间轮来优化定时器
在raft协议中, 会初始化三个计时器是和选举有关的:
voteTimer:这个timer负责定期的检查,如果当前的state的状态是候选者(STATE_CANDIDATE),那么就会发起选举
electionTimer:在一定时间内如果leader没有与 Follower 进行通信时,Follower 就可以认为leader已经不能正常担任leader的职责,那么就会进行选举,在选举之前会先发起预投票,如果没有得到半数以上节点的反馈,则候选者就会识趣的放弃参选。所以这个timer负责预投票
stepDownTimer:定时检查是否需要重新选举leader,如果当前的leader没有获得超过半数的Follower响应,那么这个leader就应该下台然后重新选举。
public boolean init(final NodeOptions opts) {....// Init timers//设置投票计时器this.voteTimer = new RepeatedTimer("JRaft-VoteTimer", this.options.getElectionTimeoutMs()) {@Overrideprotected void onTrigger() {//处理投票超时handleVoteTimeout();}@Overrideprotected int adjustTimeout(final int timeoutMs) {//在一定范围内返回一个随机的时间戳return randomTimeout(timeoutMs);}};//设置预投票计时器//当leader在规定的一段时间内没有与 Follower 舰船进行通信时,// Follower 就可以认为leader已经不能正常担任旗舰的职责,则 Follower 可以去尝试接替leader的角色。// 这段通信超时被称为 Election Timeout//候选者在发起投票之前,先发起预投票this.electionTimer = new RepeatedTimer("JRaft-ElectionTimer", this.options.getElectionTimeoutMs()) {@Overrideprotected void onTrigger() {handleElectionTimeout();}@Overrideprotected int adjustTimeout(final int timeoutMs) {//在一定范围内返回一个随机的时间戳//为了避免同时发起选举而导致失败return randomTimeout(timeoutMs);}};//leader下台的计时器//定时检查是否需要重新选举leaderthis.stepDownTimer = new RepeatedTimer("JRaft-StepDownTimer", this.options.getElectionTimeoutMs() >> 1) {@Overrideprotected void onTrigger() {handleStepDownTimeout();}};....if (!this.conf.isEmpty()) {//新启动的node需要重新选举stepDown(this.currTerm, false, new Status());}....
}
如果一个系统存在大量的任务调度,时间轮可以高效的利用线程资源来进行批量化调度。把大批量的调度任务全部都绑定时间轮上,通过时间轮进行所有任务的管理,触发以及运行。能够高效地管理各种延时任务,周期任务,通知任务等。时间轮(TimingWheel)算法应用范围非常广泛,各种操作系统的定时任务调度都有用到,我们熟悉的 Linux Crontab,以及 Java 开发过程中常用的 Dubbo、Netty、Akka、Quartz、ZooKeeper 、Kafka 等,几乎所有和 时间任务调度 都采用了时间轮的思想。
如何实现一个简易的C++时间轮
在介绍这个之前,先介绍linux定时任务的基本实现方式
一般有两个常见的比较有效的方法。一个是用 Linux 内部定时器alarm;另一个是用 sleep 或 usleep 函数让进程睡眠一段时间
如果不要求很精确的话,用 alarm() 和 signal() 就够了
unsigned int alarm(unsigned int seconds)
专门为SIGALRM信号而设,在指定的时间seconds秒后,将向进程本身发送SIGALRM信号,又称为闹钟时间。进程调用alarm后,任何以前的alarm()调用都将无效。如果参数seconds为零,那么进程内将不再包含任何闹钟时间。如果调用alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。
void sig_alarm()
{ exit(0);
}
int main(int argc, char *argv[])
{ signal(SIGALRM, sig_alarm); alarm(10); sleep(15); printf("Hello World!\n"); return 0;
}
所以当main()程序挂起10秒钟时,signal函数调用SIGALRM信号的处理函数sig_alarm,并且sig_alarm执行exit(0)使得程序直接退出。因此,printf(“Hello World!\n”)语句是没有被执行的。
如果你只做一般的定时,到了时间去执行一个任务,sleep是最简单的。但是sleep会让程序挂起,肯定不好。我们要的是可以继续执行下面的任务,时间一到,就出发信号,进行相应任务处理。除非用一个死循环专门用来定时调度。
不过,如果有多个定时任务,就需要对定时器做一些封装了。先介绍一下基于升序时间链表的计时器。
//Time_list.h
#include <netinet/in.h> //sockaddr_in
#include <list>
#include <functional>#define BUFFER_SIZE 64class util_timer; //前向声明//客户端数据
struct client_data {sockaddr_in address; //socket地址int sockfd; //socket文件描述符char buf[BUFFER_SIZE]; //数据缓存区util_timer* timer; //定时器
};//定时器类
class util_timer {
public:time_t expire; //任务超时时间(绝对时间)std::function<void(client_data*)> callBackFunc; //回调函数client_data* userData; //用户数据
};class Timer_list {public:explicit Timer_list();~Timer_list();public:void add_timer(util_timer* timer); //添加定时器void del_timer(util_timer* timer); //删除定时器void adjust_timer(util_timer* timer); //调整定时器void tick(); //处理链表上到期的任务private:std::list<util_timer*> m_timer_list; //定时器链表
};//Timer_list.cpp
#include "Timer_list.h"
#include <time.h>Timer_list::Timer_list() {}Timer_list::~Timer_list() {m_timer_list.clear();
}void Timer_list::add_timer(util_timer* timer) { //将定时器添加到链表if (!timer) return;else {auto item = m_timer_list.begin();while (item != m_timer_list.end()) {if (timer->expire < (*item)->expire) {m_timer_list.insert(item, timer);return;}item++;}m_timer_list.emplace_back(timer);}
}void Timer_list::del_timer(util_timer* timer) { //将定时器从链表删除if (!timer) return;else {auto item = m_timer_list.begin();while (item != m_timer_list.end()) {if (timer == *item) {m_timer_list.erase(item);return;}item++;}}
}void Timer_list::adjust_timer(util_timer *timer) { //调整定时器在链表中的位置del_timer(timer);add_timer(timer);
}void Timer_list::tick() { //SIGALRM信号触发,处理链表上到期的任务if (m_timer_list.empty()) return;time_t cur = time(nullptr);//检测当前定时器链表中到期的任务。while (!m_timer_list.empty()) {util_timer* temp = m_timer_list.front();if (cur < temp->expire) break;temp->callBackFunc(temp->userData);m_timer_list.pop_front();}
}
怎么运用呢服务端?
首先要设置signal(SIGALRM, sig_alarm); alarm(10)这样每10秒钟就会触发信号处理函数,函数里面调用tick处理链表上到期的任务。并且再次发送alarm,周期执行
在主循环不断接受新链接,把新链接的客户端数据绑定到计时器上(计时器包括超时时间,回调函数,用户数据),计时器加入到链表中。
如果要处理不活跃的连接,每次连接有活动,还要更新过期时间=当前时间+10s,调整在链表的位置
//SIGALRM 信号的处理函数
void timer_handler() {
timer_list.tick(); //调用升序定时器链表类的tick() 处理链表上到期的任务
alarm(TIMESLOT); //再次发出 SIGALRM 信号
}
//定时器回调函数 删除socket上注册的事件并关闭此socket
void timer_callback(client_data* user_data) {
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, user_data->sockfd, 0);
if (user_data) {
close(user_data->sockfd);
cout << "close fd : " << user_data->sockfd << endl;
}
}
因为在有序链表插入节点的时间复杂度为O(n),而且是单链表,意味着链表越长,插一个节点所要找到合适位置的时间开销就会越大,这样下来,时间效率是比较低的。
很显然,对于时间轮而言,要提高精度,就要使si的值足够小; 要提高执行效率,则要求N值足够大,使定时器尽可能的分布在不同的槽。
#define BUFFER_SIZE 64
class tw_timer;/* 绑定socket和定时器 */
struct client_data {sockaddr_in address;int sockfd;char buf[BUFFER_SIZE];tw_timer *timer;
};/* 定时器类 */
class tw_timer {
public:tw_timer(int rot, int ts) {next = nullptr;prev = nullptr;rotation = rot;time_slot = ts;}
public:int rotation; /* 记录定时器在时间轮转多少圈后生效 */int time_slot; /* 记录定时器属于时间轮上的哪个槽(对应的链表,下同) */void (*cb_func)(client_data *); /* 定时器回调函数 */client_data *user_data; /* 客户端数据 */tw_timer *next; /* 指向下一个定时器 */tw_timer *prev; /* 指向前一个定时器 */
};
定时器类和之前的不一样了。包含了前驱后驱指针,槽,回调函数。
下面是时间轮代码
**class time_wheel {
public:time_wheel();~time_wheel();/* 根据定时值timeout创建一个定时器,并把它插入到合适的槽中 */tw_timer *add_timer(int timeout);/* 删除目标定时器timer */void del_timer(tw_timer *timer);/* SI时间到后,调用该函数,时间轮向前滚动一个槽的间隔 */void tick();
private:static const int N = 60; /* 时间轮上槽的数量 */static const int SI = 1; /* 每1 s时间轮转动一次,即槽间隔为1 s */tw_timer* slots[N]; /* 时间轮的槽,其中每个元素指向一个定时器链表,链表无序 */int cur_slot; /* 时间轮的当前槽 */
};**
time_wheel::time_wheel() { cur_slot = 0;for (int i = 0; i < N; i++) {slots[i] = nullptr;}
}time_wheel::~time_wheel() {/* 遍历每个槽,并销毁其中的定时器 */for (int i = 0; i < N; i++) {/* 释放链表上的每个节点 */tw_timer *tmp = slots[i];while (tmp) {slots[i] = tmp->next;delete tmp;tmp = slots[i];}}
}tw_timer *time_wheel::add_timer(int timeout) {if (timeout < 0) {return nullptr;}int ticks = 0;/* 下面根据带插入定时器的超时值计算它将在时间轮转动多少个滴答后被触发,并将该滴答* 数存储于变量ticks中。如果待插入定时器的超时值小于时间轮的槽间隔SI,则将ticks* 向上折合为1,否则就将ticks向下折合为timeout/SI */if (timeout < SI) {ticks = 1;} else {ticks = timeout / SI;}/* 计算待插入的定时器在时间轮转动多少圈后被触发 */int rotation = ticks / N;/* 计算待插入的定时器应该被插入到哪个槽中 */int ts = (cur_slot + (ticks % N)) % N;/* 创建新的定时器,它在时间轮转动ratation圈之后被触发,且位于第ts个槽上 */tw_timer *timer = new tw_timer(rotation, ts);/* 如果第ts个槽中无任何定时器,则把新建的定时器插入其中,并将该定时器设置为该槽的头结点 */if (slots[ts] == nullptr) {printf("add timer, rotation is %d,cur_slot is %d\n", rotation, ts, cur_slot);slots[ts] = timer;} else {/* 头插法在链表中插入节点 */timer->next = slots[ts];slots[ts]->prev = timer;slots[ts] = timer;}return timer;
}void time_wheel::del_timer(tw_timer *timer) {if (timer == nullptr) {return;}int ts = timer->time_slot;/* slots[ts]是目标定时器所在槽的头结点。如果目标定时器就是该头结点,则需要* 重置第ts个槽的头结点 */if (timer == slots[ts]) {slots[ts] = slots[ts]->next;if (slots[ts]) {slots[ts]->prev = nullptr;}delete timer;} else {timer->prev->next = timer->next;if (timer->next) {timer->next->prev = timer->prev;}delete timer;}
}void time_wheel::tick() {tw_timer *tmp = slots[cur_slot]; /* 取得时间轮上当前槽的头结点 */printf("current slots is %d\n", cur_slot);while (tmp) {printf("tick the timer once\n");/* 如果定时器的ratation值大于0,则它在这一轮中不起作用 */if (tmp->rotation > 0) {tmp->rotation--;tmp = tmp->next;}/* 否则说明定时器已经到期,于是执行定时任务,然后删除该定时器 */else {tmp->cb_func(tmp->user_data);if (tmp == slots[cur_slot]) {printf("delete header in cur_slot\n");slots[cur_slot] = tmp->next;delete tmp;if (slots[cur_slot]) {slots[cur_slot]->prev = nullptr;}tmp = slots[cur_slot];} else {tmp->prev->next = tmp->next;if (tmp->next) {tmp->next->prev = tmp->prev;}tw_timer *tmp2 = tmp->next;delete tmp;tmp = tmp2;}}}/* 更新时间轮的当前槽,以反映时间轮的转动 */cur_slot = cur_slot + 1;cur_slot = cur_slot % N;
}
时间轮还可以用多个不同精度的时间轮(内存优化),最小堆进行优化。
相关文章:

时间轮来优化定时器
在raft协议中, 会初始化三个计时器是和选举有关的: voteTimer:这个timer负责定期的检查,如果当前的state的状态是候选者(STATE_CANDIDATE),那么就会发起选举 electionTimer:在一定时…...
《和AI交朋友》教学设计——初识人工智能
创新整合点 (1借助编程软件的机器学习扩展,使学生初步体验建立训练模型,让电脑进行学习的过程,进而感受人工智能的核心技术之一。 (2)借助编程软件的人工智能服务, 在编写程序时使用语音交互模块…...

机载雷达的时间简史
从地基起步 蝙蝠,虽然像人一样拥有双眼,但它看起东西来,用到的却不是眼睛。蝙蝠从鼻子里发出的超声波在传输过程中遇到物体后会立刻反弹,根据声波发射和回波接收之间的时间差,蝙蝠就可以轻易地判断出物体的位置。这一工…...

2018年MathorCup数学建模A题矿相特征迁移规律研究解题全过程文档及程序
2018年第八届MathorCup高校数学建模挑战赛 A题 矿相特征迁移规律研究 原题再现: 背景材料: 球团矿具有含铁品位高、粒度均匀、还原性能好、机械强度高、微气孔多等特性, 是高炉炼铁的重要原料之一。近年来国内外普遍认识到球团矿高温状态下冶金性能是评价炉料…...
如何在 Python 中创建对象列表
Python 中要创建对象列表: 声明一个新变量并将其初始化为一个空列表。使用 for 循环迭代范围对象。实例化一个类以在每次迭代时创建一个对象。将每个对象附加到列表中。 class Employee():def __init__(self, id):self.id idlist_of_objects []for i in range(5…...

Canny算法原理和应用
Canny算法的原理使用高斯滤波器滤波使用 Sobel 滤波器滤波获得在 x 和 y 方向上的输出,在此基础上求出梯度的强度和梯度的角度edge为边缘强度,tan为梯度方向上图表示的是中心点的梯度向量、方位角以及边缘方向(任一点的边缘与梯度向量正交&am…...

数据挖掘(2.2)--数据预处理
目录 二、数据描述 1.描述数据中心趋势 1.1平均值和截断均值 1.2加权平均值 1.3中位数(Median)和众数(Mode) 2.描述数据的分散程度 2.1箱线图 2.2方差和标准差 2.3正态分布 3.数据清洗 3.1数据缺失的处理 3.2数据清洗 二、数据描述 描述数…...

JVM堆与堆调优以及出现OOM如何排查
调优的位置——堆 Heap,一个JVM只有一个堆内存,堆内存的大小是可以调节的。 类加载器读取了类文件后,一般会把什么东西放到堆中?类,方法,常量,变量~,保存我们所有引用类型的真实对象; 堆内存中…...

Springboot——自定义Filter使用测试总结
文章目录前言自定义过滤器并验证关于排除某些请求的方式创建测试接口请求测试验证异常过滤器的执行流程注意事项资料参考前言 在Java-web的开发领域,对于过滤器和拦截器用处还是很多,但两者的概念却极易混淆。 过滤器和拦截器都是采用AOP的核心思想&am…...

软件测试(进阶篇)(1)
一)如何根据需求来设计测试用例? 1)验证功能的正确性,合理性,无二义性,逻辑要正确 2)分析需求,细化需求,从需求中提取出测试项,根据测试项找到测试点,根据测试点具体的来进行设计测试…...
(七十三)大白话深入探索多表关联的SQL语句到底是如何执行的?(1)
今天我们来继续跟大家聊聊多表关联语句是如何执行的这个问题,上次讲了一个最最基础的两个表关联的语句和执行过程,其实今天我们稍微来复习一下,然后接着上次的内容,引入一个“内连接”的概念来。 假设我们有一个员工表࿰…...

SYSU程设c++(第三周) 对象类、类的成员、类与结构体的区别、类的静态成员
对象&类 类用于指定对象的形式,它包含数据的表示方法和用于处理数据的方法。 • 类中的数据和方法称为类的成员。 • 函数在一个类中也被称为类的成员。 定义一个类,其效果是定义一个数据类型的蓝图。它定义了类的对象包括了什么,以及可…...
Redis管道
目录 1、什么是管道? 2、案例演示 3、注意事项 4、面试题 1、什么是管道? 管道(pipeline)可以一次性发送多条命令给服务端,服务端依次处理完,通过一条响应一次性将结果返回,减少 IO 的次数&…...
conda的共用package[硬链接]@pytorch和tensorflow装在同一个环境里好不好?
文章目录refpackage复用(指定同版本)conda install 比pip install 更可能节省空间pytorch和tensorflow装在同一个环境里?导入依赖导入依赖试验ref python - Can packages be shared across Anaconda environments? - Stack OverflowManaging environments — conda 23.1.0.p…...

「Vue面试题」动态给vue的data添加一个新的属性时会发生什么?怎样去解决的?
一、直接添加属性的问题 我们从一个例子开始 定义一个p标签,通过v-for指令进行遍历 然后给botton标签绑定点击事件,我们预期点击按钮时,数据新增一个属性,界面也 新增一行 <p v-for"(value,key) in item" :key&q…...

Flutter-Scaffold组件
在Flutter开发当中,我们可能会遇到以下的需求:实现页面组合使用,比如说有悬浮按钮、顶部菜单栏、左右抽屉侧边栏、底部导航栏等等效果。Scaffold组件可以帮我们实现上面需求说的效果。这篇博客主要分享容器组件的Scaffold组件的使用ÿ…...

Postman简介及接口测试流程(小菜鸟攻略)
目录 前言 一、常见接口 二、前端和后端 三、什么是接口测试 四、接口组成 1、接口说明 2、调用url 3、请求方法(get\post) 4、请求参数、参数类型、请求参数说明 5、返回参数说明 五、为什么要做接口测试 本章主要介绍如何使用postman做接口…...
kubebuilder注释
标记语法Empty kubebuilder:validation:Optional:空标记像命令行中的布尔标记位-- 仅仅是指定他们来开启某些行为。Anonymous kubebuilder:validation:MaxItems2:匿名标记使用单个值作为参数。Multioption kubebuilder:printcolumn:JSONPath".statu…...

java日志
日志是软件开发的重要组成部分。一个精心编写的日志代码提供快速的调试,维护方便,以及应用程序的运行时信息结构化存储。日志记录确实也有它的缺点。它可以减缓的应用程序Log4jLog4j是Apache的一个开放源代码项目,通过使用Log4j,我…...

研发中台拆分过程的一些心得总结
背景在 21 年,中台拆分在 21 年,以下为中台拆分的过程心得,带有一定的主观,偏向于中小团队中台建设参考(这里的中小团队指 3-100 人的团队),对于大型团队不太适用,毕竟大型团队人中 …...
基于服务器使用 apt 安装、配置 Nginx
🧾 一、查看可安装的 Nginx 版本 首先,你可以运行以下命令查看可用版本: apt-cache madison nginx-core输出示例: nginx-core | 1.18.0-6ubuntu14.6 | http://archive.ubuntu.com/ubuntu focal-updates/main amd64 Packages ng…...
Axios请求超时重发机制
Axios 超时重新请求实现方案 在 Axios 中实现超时重新请求可以通过以下几种方式: 1. 使用拦截器实现自动重试 import axios from axios;// 创建axios实例 const instance axios.create();// 设置超时时间 instance.defaults.timeout 5000;// 最大重试次数 cons…...
AI编程--插件对比分析:CodeRider、GitHub Copilot及其他
AI编程插件对比分析:CodeRider、GitHub Copilot及其他 随着人工智能技术的快速发展,AI编程插件已成为提升开发者生产力的重要工具。CodeRider和GitHub Copilot作为市场上的领先者,分别以其独特的特性和生态系统吸引了大量开发者。本文将从功…...

用机器学习破解新能源领域的“弃风”难题
音乐发烧友深有体会,玩音乐的本质就是玩电网。火电声音偏暖,水电偏冷,风电偏空旷。至于太阳能发的电,则略显朦胧和单薄。 不知你是否有感觉,近两年家里的音响声音越来越冷,听起来越来越单薄? —…...
Java + Spring Boot + Mybatis 实现批量插入
在 Java 中使用 Spring Boot 和 MyBatis 实现批量插入可以通过以下步骤完成。这里提供两种常用方法:使用 MyBatis 的 <foreach> 标签和批处理模式(ExecutorType.BATCH)。 方法一:使用 XML 的 <foreach> 标签ÿ…...

[ACTF2020 新生赛]Include 1(php://filter伪协议)
题目 做法 启动靶机,点进去 点进去 查看URL,有 ?fileflag.php说明存在文件包含,原理是php://filter 协议 当它与包含函数结合时,php://filter流会被当作php文件执行。 用php://filter加编码,能让PHP把文件内容…...

PHP 8.5 即将发布:管道操作符、强力调试
前不久,PHP宣布了即将在 2025 年 11 月 20 日 正式发布的 PHP 8.5!作为 PHP 语言的又一次重要迭代,PHP 8.5 承诺带来一系列旨在提升代码可读性、健壮性以及开发者效率的改进。而更令人兴奋的是,借助强大的本地开发环境 ServBay&am…...
Python常用模块:time、os、shutil与flask初探
一、Flask初探 & PyCharm终端配置 目的: 快速搭建小型Web服务器以提供数据。 工具: 第三方Web框架 Flask (需 pip install flask 安装)。 安装 Flask: 建议: 使用 PyCharm 内置的 Terminal (模拟命令行) 进行安装,避免频繁切换。 PyCharm Terminal 配置建议: 打开 Py…...

基于stm32F10x 系列微控制器的智能电子琴(附完整项目源码、详细接线及讲解视频)
注:文章末尾网盘链接中自取成品使用演示视频、项目源码、项目文档 所用硬件:STM32F103C8T6、无源蜂鸣器、44矩阵键盘、flash存储模块、OLED显示屏、RGB三色灯、面包板、杜邦线、usb转ttl串口 stm32f103c8t6 面包板 …...

篇章一 论坛系统——前置知识
目录 1.软件开发 1.1 软件的生命周期 1.2 面向对象 1.3 CS、BS架构 1.CS架构编辑 2.BS架构 1.4 软件需求 1.需求分类 2.需求获取 1.5 需求分析 1. 工作内容 1.6 面向对象分析 1.OOA的任务 2.统一建模语言UML 3. 用例模型 3.1 用例图的元素 3.2 建立用例模型 …...