时间轮来优化定时器

news2024/9/29 9:21:59

在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()) {

        @Override
        protected void onTrigger() {
            //处理投票超时
            handleVoteTimeout();
        }

        @Override
        protected 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()) {

        @Override
        protected void onTrigger() {
            handleElectionTimeout();
        }

        @Override
        protected int adjustTimeout(final int timeoutMs) {
            //在一定范围内返回一个随机的时间戳
            //为了避免同时发起选举而导致失败
            return randomTimeout(timeoutMs);
        }
    };
    //leader下台的计时器
    //定时检查是否需要重新选举leader
    this.stepDownTimer = new RepeatedTimer("JRaft-StepDownTimer", this.options.getElectionTimeoutMs() >> 1) {

        @Override
        protected 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 64

class 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;
}

时间轮还可以用多个不同精度的时间轮(内存优化),最小堆进行优化。

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

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

相关文章

Photoshop如何安装ZXP扩展插件?

Photoshop如何安装ZXP扩展插件呢&#xff1f;有一些小伙伴不会安装&#xff0c;今天介绍两种安装ZXP扩展的方法&#xff0c;希望对能帮助到大家。方法一&#xff1a;手动安装方式1&#xff09;把下载好的.zxp扩展名改为.zip&#xff0c;然后解压。Windows系统&#xff1a;C:\Us…

参考文献去哪里查找,参考文献标准格式是什么

1、参考文献类型&#xff1a; 普通图书[M]、期刊文章[J]、报纸文章[N]、论文集[C]、学位论 文[D]、报告[R]、标准[s]、专利[P]、数据库[DB]、计算机程序[CP]、电 子公告[EB]、联机网络[OL]、网上期刊[J&#xff0f;OL]、网上电子公告[EB&#xff0f;OL]、其他未 说明文献[z]。…

I.MX6ULL_Linux_驱动篇(28) 字符设备驱动

字符设备驱动简介 字符设备是 Linux 驱动中最基本的一类设备驱动&#xff0c;字符设备就是一个一个字节&#xff0c;按照字节流进行读写操作的设备&#xff0c;读写数据是分先后顺序的。比如我们最常见的点灯、按键、 IIC、 SPI&#xff0c;LCD 等等都是字符设备&#xff0c;这…

蓝牙耳机品牌哪个质量好?2023质量好的蓝牙耳机推荐

相较于有线耳机&#xff0c;蓝牙耳机凭借更方便地使用而受到不少人的喜爱&#xff0c;蓝牙耳机各种性能的发展也越来越先进。哪个品牌的蓝牙耳机质量好&#xff1f;针对这个问题&#xff0c;我来给大家推荐几款质量好的蓝牙耳机&#xff0c;一起来看看吧。 一、南卡小音舱蓝牙耳…

接口请求参数异常之@RequestParam

问题 具体问题如下&#xff1a; 正确的如下&#xff1a; 如上两图所示&#xff1a;我们的请求参数是pageNo和pageSize&#xff0c;但是却没有正确显示&#xff0c;则说明我们的接口存在了问题。 分析问题 参数为什么没有正确的显示每页显示条数和当前页数&#xff0c;而是我…

jmeter-定时器

记录下业务中常用的定时器概念以及使用流程&#xff0c;仅供自己学习。 定时器的作用域 1、定时器是在每个sampler&#xff08;采样器&#xff09;之前执行的&#xff0c;而不是之后&#xff08;无论定时器位置在sampler之前还是下面&#xff09;&#xff1b; 2、当执行一个sa…

Apache HTTP Server <2.4.56 mod_proxy 模块存在请求走私漏洞(CVE-2023-25690)

漏洞描述 Apache HTTP Server是一款Web服务器。 该项目受影响版本存在请求走私漏洞。由于intro.xml中存在RewriteRule配置不当&#xff0c;当Apache启用mod_proxy且配置如 RewriteRule “^/here/(.*)” " http://example.com:8080/elsewhere?$1"; http://example.…

Elasticsearch 核心技术(七):IK 中文分词器的安装、使用、自定义字典

❤️ 博客主页&#xff1a;水滴技术 &#x1f680; 支持水滴&#xff1a;点赞&#x1f44d; 收藏⭐ 留言&#x1f4ac; &#x1f338; 订阅专栏&#xff1a;大数据核心技术从入门到精通 文章目录一、安装 IK 分词器方式一&#xff1a;自行下载并解压安装包方式二&#xff1a;…

【前端学习】D3:CSS进阶

文章目录前言系列文章目录1 CSS的三大特性1.1 层叠性1.2 继承性1.3 优先级&#xff08;*&#xff09;2 盒子模型2.1 看透网页布局的本质2.2 盒子模型&#xff08;Box Model&#xff09;的组成2.3 边框&#xff08;border&#xff09;2.3.1 普通边框2.3.2 表格的细线边框2.3.3 边…

【Hello Linux】命令行解释器

作者&#xff1a;小萌新 专栏&#xff1a;Linux 作者简介&#xff1a;大二学生 希望能和大家一起进步&#xff01; 本篇博客简介&#xff1a;使用进程的基础知识和进程控制知识做出一个简单的shell程序 命令行解释器介绍搭架子缓冲区获取命令如何从标准输入中获取字符串解析命令…

Python3 爬虫实战教程 ,网页审查元素【Python学习连续,请关注】

在讲解爬虫内容之前&#xff0c;我们需要先学习一项写爬虫的必备技能&#xff1a;审查元素&#xff08;如果已掌握&#xff0c;可跳过此部分内容&#xff09;。1、审查元素在浏览器的地址栏输入URL地址&#xff0c;在网页处右键单击&#xff0c;找到检查。(不同浏览器的叫法不同…

爬虫(三)selenium

文章目录1. Selenium 安装2. Selenium 基本功能2.1 初始化浏览器2.2 其他功能3. 查找元素3.1 八大定位方法3.2 查找相对元素3.3 键盘事件4. 元素方法5. JS执行运行环境&#xff1a; selenium4.7.2 1. Selenium 安装 Selenium是一个用于Web应用程序测试的工具。Selenium测试直接…

成都欢蓬电商:抖音带话题春日好物节活动规则

抖音带话题“春日好物节”&#xff0c;投稿瓜分优质内容激励&#xff0c;快来投稿参与本次抖音活动&#xff01; 一、活动玩法 活动时间&#xff1a;3月3日-3月16日 活动形式&#xff1a; 玩法说明&#xff1a; 若同一id同时参加获奖&#xff0c;则不重复激励; 因视频投流涉…

易基因:RRBS揭示晚年锻炼可以减缓骨骼肌表观遗传衰老(甲基化年龄)|新研究

大家好&#xff0c;这里是专注表观组学十余年&#xff0c;领跑多组学科研服务的易基因。2021年12月21日&#xff0c;美国阿肯色大学、德克萨斯大学和肯塔基大学的研究人员合作在《Aging Cell》杂志发表了题为“Late-life exercise mitigates skeletal muscle epigenetic aging”…

还是要学好数学啊

有一个无穷大的二维网格图&#xff0c;一开始所有格子都未染色。给你一个正整数 n &#xff0c;表示你需要执行以下步骤 n 分钟&#xff1a;第一分钟&#xff0c;将任一格子染成蓝色。之后的每一分钟&#xff0c;将与蓝色格子相邻的 所有 未染色格子染成蓝色。下图分别是 1、2、…

每日一问-ChapGPT-20230308-关于技术与思考的问题

文章目录每日一问-ChapGPT系列起因每日一问-ChapGPT-20230308-关于技术与思考的问题matplotlib_venn 中 venn2函数调用时&#xff0c;subsets传入A list (or a tuple) containing two set objects&#xff0c;怎么理解plt.pie() 包含哪些参数&#xff0c;以及每个参数的意义mat…

云端地球2月更新了这些功能,你都用过了吗?

时光飞逝、转眼已到2023年的第三个月&#xff0c;武汉的天气也逐渐转好&#xff0c;温度步步高升。云端地球产研团队的脚步也越走越快&#xff0c;虽然春节仿佛还是昨天的事&#xff0c;但云端地球已经完成了四次迭代&#xff0c;为广大建模爱好者带来了更多实用功能&#xff0…

BOSHIDA 模块电源的安装与维护

BOSHIDA三河博电科技 模块电源的安装与维护 由于各生产的模块电源的类别、系列、规格品种难以数计&#xff0c;故其功能特性和物理特性不尽相同&#xff0c;因此在安装、使用与维护方面亦各有不同&#xff0c;但应在以下几方面引起注意。 &#xff08;1&#xff09;打开包装后…

【Redis】搭建哨兵集群

目录 集群结构 准备实例和配置 启动 测试 集群结构 这里我们搭建一个三节点形成的Sentinel集群&#xff0c;来监管之前的Redis主从集群。如图&#xff1a; 三个sentinel实例信息如下&#xff1a; 节点IPPORTs1192.168.150.10127001s2192.168.150.10127002s3192.168.150.…

10 卷积神经网络及python实现

1 卷积神经网络简介 卷积神经网络(Convolutional Neural Network, CNN)由LeCun在上世纪90年代提出。 LeCun Y., Bottou L., Bengio Y., and Haffner P., Gradient-based learning applied to document recognition, Proceedings of the IEEE, pp. 1-7, 1998. 卷积核和特征图&…