网易面试:手撕定时器

news2024/11/16 21:29:12

概述:

本文使用STL容器-set以及Linux提供的timerfd来实现定时器组件

所谓定时器就是管理大量定时任务,使其能按照超时时间有序地被执行

需求分析:

1.数据结构的选择:存储定时任务

2.驱动方式:如何选择一个任务并执行

一、数据结构的选择:红黑树

红黑树是一种查找、删除、插入时间复杂度为O(logN)的数据结构,性能均衡,STL的set和map就是基于红黑树实现的,与普通的红黑树不同,STL的红黑树设计添加了指向最大节点和最小节点的指针,这一点实现了set和map可以使用O(1)的时间复杂度查找最大值和最小值:

在这里插入图片描述

二、驱动方式:timerfd + epoll

Linux提供了定时机制timerfd,与sockfd一样,内核负责检测该文件描述符的就绪情况,并需要epoll等io多路复用机制向用户层通知

三、代码实现

1.定时任务节点:

struct TimerNodeBase { //  定时器节点基类,用于红黑树(set)存储
    time_t expire;     //  超时时间
    uint64_t id;       //  唯一 id, 用于解决超时时间相同的节点存储问题
};

struct TimerNode : public TimerNodeBase {  // 子类定时器节点, 添加了一个回调函数
    using Callback = function<void(const TimerNode &node)>;
    Callback func;
    TimerNode(int64_t id, time_t expire, Callback func) : func(std::move(func)) { // 使用 move 右值引用,性能高
        this->expire = expire;
        this->id = id;
    }
};

这里设计两个结构体的目的是将回调函数和其它属性(超时时间和id)隔离开

2.运算符重载,用于比较两个节点的大小(超时时间大小)

bool operator < (const TimerNodeBase &lhd, const TimerNodeBase &rhd) { // 运算符重载,比较两个节点的大小
    // 先根据超时时间判定大小
    if (lhd.expire < rhd.expire) {
        return true;
    } else if (lhd.expire > rhd.expire) {
        return false;
    } // 超时时间相同时,根据 id 判断大小
    else return lhd.id < rhd.id;
}

3.定时器类:

class Timer {

public:
    static inline time_t GetTick() { // 获取系统当前时间戳
        return chrono::duration_cast<chrono::milliseconds>(chrono::steady_clock::now().time_since_epoch()).count();
    }

    TimerNodeBase AddTimer(int msec, TimerNode::Callback func) {
        time_t expire = GetTick() + msec; // msec是相对超时时间,expire是绝对超时时间(时间戳)
        // 如果待插入节点当前不是红黑树中最大的
        if (timeouts.empty() || expire <= timeouts.crbegin()->expire) { 
            auto pairs = timeouts.emplace(GenID(), expire, std::move(func)); // emplace是在容器内部生成一个对象并插入到红黑树中,性能优于push的copy操作  2.使用move右值引用,避免copy
            // 使用static_cast将子类cast成基类
            return static_cast<TimerNodeBase>(*pairs.first); // emplace的返回值pair包含:1.创建并插入的节点 2.是否成功插入(已存在相同节点则插入失败)
        }
        // 如果待插入节点是最大的,直接插入到最右侧,时间复杂度 O(1) ,优化性能
        auto ele = timeouts.emplace_hint(timeouts.crbegin().base(), GenID(), expire, std::move(func));
       // 返回基类而不是子类
        return static_cast<TimerNodeBase>(*ele);
    }

    void DelTimer(TimerNodeBase &node) { // 从(set)红黑树中删除一个节点
        auto iter = timeouts.find(node); // 找到指定节点
        if (iter != timeouts.end())
            timeouts.erase(iter);       // 移除
    }
    
    void HandleTimer(time_t now) {     // 执行当前已超时的任务
        auto iter = timeouts.begin();
        while (iter != timeouts.end() && iter->expire <= now) {
            iter->func(*iter);
            iter = timeouts.erase(iter); // eraser返回下一个节点
        }
    }

public:
    // 更新 timerfd 的到期时间为 timeouts 集合中最早到期的定时器时间
    virtual void UpdateTimerfd(const int fd) {
        struct timespec abstime;
        auto iter = timeouts.begin();  // 最小超时时间节点
        if (iter != timeouts.end()) {
            abstime.tv_sec = iter->expire / 1000;
            abstime.tv_nsec = (iter->expire % 1000) * 1000000;
        } else {
            abstime.tv_sec = 0;
            abstime.tv_nsec = 0;
        }

        struct itimerspec its;
        its.it_interval = {};
        its.it_value = abstime;

        timerfd_settime(fd, TFD_TIMER_ABSTIME, &its, nullptr);
    }

private:
    static inline uint64_t GenID() { // 生成一个 id
        return gid++;
    }
    static uint64_t gid; // 全局 id 变量

    set<TimerNode, std::less<> > timeouts; // less指定排序方式:从小到大
};

在class timer中实现了

1.创建set容器对象

2.定时任务节点的添加

3.定时任务节点的删除

4.执行已超时任务

四、main函数

int main() {
    int epfd = epoll_create(1);  // epoll

    int timerfd = timerfd_create(CLOCK_MONOTONIC, 0);
    
    struct epoll_event ev = {.events = EPOLLIN | EPOLLET};
    epoll_ctl(epfd, EPOLL_CTL_ADD, timerfd, &ev);

    unique_ptr<Timer> timer = make_unique<Timer>();
    int i = 0;
    timer->AddTimer(1000, [&](const TimerNode &node) {      //   lamda 表达式
        cout << Timer::GetTick() << "node id:" << node.id << "revoked times" << ++i << endl;
    });

    timer->AddTimer(1000, [&](const TimerNode &node) {
        cout << Timer::GetTick() << " node id:" << node.id << " revoked times:" << ++i << endl;
    });

    timer->AddTimer(3000, [&](const TimerNode &node) {
        cout << Timer::GetTick() << " node id:" << node.id << " revoked times:" << ++i << endl;
    });

    auto node = timer->AddTimer(2100, [&](const TimerNode &node) {
        cout << Timer::GetTick() << " node id:" << node.id << " revoked times:" << ++i << endl;
    });
    timer->DelTimer(node);

    cout << "now time:" << Timer::GetTick() << endl;

    struct epoll_event evs[64] = {0};
    while (true) {
        timer->UpdateTimerfd(timerfd);    // epoll中timerfd的到期时间
        int n = epoll_wait(epfd, evs, 64, -1); // 内核检测定时时间timerfd
        time_t now = Timer::GetTick();   // 当前系统时间戳
        
        for (int i = 0; i < n; i++) {     
            // for network event handle
        }
        timer->HandleTimer(now);         // 处理现在到期的定时任务
    }
    epoll_ctl(epfd, EPOLL_CTL_DEL, timerfd, &ev);
    close(timerfd);
    close(epfd);

    return 0;
}

timerfd的使用:

1.将timerfd添加到epoll中

2.使用timerfd_settime()函数更新timerfd的超时时间

3.当超时时间到达时,epoll会通知事件

4.执行完到期的任务后,更新timerfd的超时时间为红黑树中最小节点的超时时间,epoll会再次进行通知

五、完整代码

#include <sys/epoll.h>
#include <sys/timerfd.h>
#include <time.h>
#include <unistd.h>

#include <functional>
#include <chrono>
#include <set>
#include <memory>
#include <iostream>

using namespace std;

struct TimerNodeBase { //  定时器节点基类,用于红黑树(set)存储
    time_t expire;     //  超时时间
    uint64_t id;       //  唯一 id, 用于解决超时时间相同的节点存储问题
};

struct TimerNode : public TimerNodeBase {  // 子类定时器节点, 添加了一个回调函数
    using Callback = function<void(const TimerNode &node)>;
    Callback func;
    TimerNode(int64_t id, time_t expire, Callback func) : func(std::move(func)) { // 使用 move 右值引用,性能高
        this->expire = expire;
        this->id = id;
    }
};

bool operator < (const TimerNodeBase &lhd, const TimerNodeBase &rhd) { // 运算符重载,比较两个节点的大小
    // 先根据超时时间判定大小
    if (lhd.expire < rhd.expire) {
        return true;
    } else if (lhd.expire > rhd.expire) {
        return false;
    } // 超时时间相同时,根据 id 判断大小
    else return lhd.id < rhd.id;
}


class Timer {

public:
    static inline time_t GetTick() { // 获取系统当前时间戳
        return chrono::duration_cast<chrono::milliseconds>(chrono::steady_clock::now().time_since_epoch()).count();
    }

    TimerNodeBase AddTimer(int msec, TimerNode::Callback func) {
        time_t expire = GetTick() + msec; // msec是相对超时时间,expire是绝对超时时间(时间戳)
        // 如果待插入节点当前不是红黑树中最大的
        if (timeouts.empty() || expire <= timeouts.crbegin()->expire) { 
            auto pairs = timeouts.emplace(GenID(), expire, std::move(func)); // emplace是在容器内部生成一个对象并插入到红黑树中,性能优于push的copy操作  2.使用move右值引用,避免copy
            // 使用static_cast将子类cast成基类
            return static_cast<TimerNodeBase>(*pairs.first); // emplace的返回值pair包含:1.创建并插入的节点 2.是否成功插入(已存在相同节点则插入失败)
        }
        // 如果待插入节点是最大的,直接插入到最右侧,时间复杂度 O(1) ,优化性能
        auto ele = timeouts.emplace_hint(timeouts.crbegin().base(), GenID(), expire, std::move(func));
       // 返回基类而不是子类
        return static_cast<TimerNodeBase>(*ele);
    }

    void DelTimer(TimerNodeBase &node) { // 从(set)红黑树中删除一个节点
        auto iter = timeouts.find(node); // 找到指定节点
        if (iter != timeouts.end())
            timeouts.erase(iter);       // 移除
    }
    
    void HandleTimer(time_t now) {     // 执行当前已超时的任务
        auto iter = timeouts.begin();
        while (iter != timeouts.end() && iter->expire <= now) {
            iter->func(*iter);
            iter = timeouts.erase(iter); // eraser返回下一个节点
        }
    }

public:
    // 更新 timerfd 的到期时间为 timeouts 集合中最早到期的定时器时间
    virtual void UpdateTimerfd(const int fd) {
        struct timespec abstime;
        auto iter = timeouts.begin();  // 最小超时时间节点
        if (iter != timeouts.end()) {
            abstime.tv_sec = iter->expire / 1000;
            abstime.tv_nsec = (iter->expire % 1000) * 1000000;
        } else {
            abstime.tv_sec = 0;
            abstime.tv_nsec = 0;
        }

        struct itimerspec its;
        its.it_interval = {};
        its.it_value = abstime;

        timerfd_settime(fd, TFD_TIMER_ABSTIME, &its, nullptr);
    }

private:
    static inline uint64_t GenID() { // 生成一个 id
        return gid++;
    }
    static uint64_t gid; // 全局 id 变量

    set<TimerNode, std::less<> > timeouts; // less指定排序方式:从小到大
};

uint64_t Timer::gid = 0;


int main() {
    int epfd = epoll_create(1);  // epoll

    int timerfd = timerfd_create(CLOCK_MONOTONIC, 0);
    
    struct epoll_event ev = {.events = EPOLLIN | EPOLLET};
    epoll_ctl(epfd, EPOLL_CTL_ADD, timerfd, &ev);

    unique_ptr<Timer> timer = make_unique<Timer>();
    int i = 0;
    timer->AddTimer(1000, [&](const TimerNode &node) {      //   lamda 表达式
        cout << Timer::GetTick() << "node id:" << node.id << "revoked times" << ++i << endl;
    });

    timer->AddTimer(1000, [&](const TimerNode &node) {
        cout << Timer::GetTick() << " node id:" << node.id << " revoked times:" << ++i << endl;
    });

    timer->AddTimer(3000, [&](const TimerNode &node) {
        cout << Timer::GetTick() << " node id:" << node.id << " revoked times:" << ++i << endl;
    });

    auto node = timer->AddTimer(2100, [&](const TimerNode &node) {
        cout << Timer::GetTick() << " node id:" << node.id << " revoked times:" << ++i << endl;
    });
    timer->DelTimer(node);

    cout << "now time:" << Timer::GetTick() << endl;

    struct epoll_event evs[64] = {0};
    while (true) {
        timer->UpdateTimerfd(timerfd);    // epoll中timerfd的到期时间
        int n = epoll_wait(epfd, evs, 64, -1); // 内核检测定时时间timerfd
        time_t now = Timer::GetTick();   // 当前系统时间戳
        
        for (int i = 0; i < n; i++) {     
            // for network event handle
        }
        timer->HandleTimer(now);         // 处理现在到期的定时任务
    }
    epoll_ctl(epfd, EPOLL_CTL_DEL, timerfd, &ev);
    close(timerfd);
    close(epfd);

    return 0;
}



推荐学习 https://xxetb.xetslk.com/s/p5Ibb

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

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

相关文章

在HTML和CSS当中运用显示隐藏

1.显示与隐藏 盒子显示:display:block;盒子隐藏: display:none:隐藏该元素并且该元素所占的空间也不存在了。 visibility:hidden:隐藏该元素但是该元素所占的内存空间还存在&#xff0c;即“隐身效果”。 2.圆角边框 在CSS2中添加圆角&#xff0c;我们不得不使用背景图像&am…

redis面试知识点

Redis知识点 Redis的RDB和AOF机制各是什么&#xff1f;它们有什么区别&#xff1f; 答&#xff1a;Redis提供了RDB和AOF两种数据持久化机制&#xff0c;适用于不同的场景。 RDB是通过在特定的时刻对内存中的完整的数据复制快照进行持久化的。 RDB工作原理&#xff1a; 当执行…

Python 机器学习 基础 之 无监督学习 【聚类(clustering)/k均值聚类/凝聚聚类/DBSCAN】的简单说明

Python 机器学习 基础 之 无监督学习 【聚类&#xff08;clustering&#xff09;/k均值聚类/凝聚聚类/DBSCAN】的简单说明 目录 Python 机器学习 基础 之 无监督学习 【聚类&#xff08;clustering&#xff09;/k均值聚类/凝聚聚类/DBSCAN】的简单说明 一、简单介绍 二、聚类…

Vue3兼容低版本浏览器(ie11,chrome63)

1、插件安装 为了使你的项目兼容 Chrome 63&#xff0c;你需要确保包含适当的 polyfills 和插件配置。你已经在使用 legacy 插件&#xff0c;但在代码中可能缺少一些配置或插件顺序有问题。以下是几个可能的改进&#xff1a; 安装 vitejs/plugin-legacy 插件&#xff1a; 确保…

Midjourney保姆级教程(五):Midjourney图生图

Midjourney生成图片的方式除了使用文字描述生成图片外&#xff0c;还有“图生图”的方式&#xff0c;可以让生成的图片更接近参考的图片。 今天我们来聊聊“图生图”的方式。 一、模仿获取propmt 很多时候&#xff0c;我们不知道画什么内容的图片&#xff0c;大家可以关注内…

一款拥有15000+POC漏洞扫描工具

1 工具介绍 0x01 免责声明 请勿使用本文中所提供的任何技术信息或代码工具进行非法测试和违法行为。若使用者利用本文中技术信息或代码工具对任何计算机系统造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责。本文所提供的技术信息或代码工具仅供于学习&am…

vue3快速上手笔记(尚硅谷)

[TOC]# 1. Vue3简介 2020年9月18日&#xff0c;Vue.js发布版3.0版本&#xff0c;代号&#xff1a;One Piece&#xff08;n 经历了&#xff1a;4800次提交、40个RFC、600次PR、300贡献者 官方发版地址&#xff1a;Release v3.0.0 One Piece vuejs/core 截止2023年10月&#…

经典必读:智能制造数字化工厂建设方案

导语 大家好&#xff0c;我是社长&#xff0c;老K。专注分享智能制造和智能仓储物流等内容。 新书《智能物流系统构成与技术实践》 完整版文件和更多学习资料&#xff0c;请球友到知识星球【智能仓储物流技术研习社】自行下载 战略背景&#xff1a;响应《中国制造2025》及"…

Kibana使用教程

Kibana使您能够轻松地向Elasticsearch发送请求&#xff0c;并以交互方式分析、可视化和管理数据。 1.安装 1.1 docker安装Kibana 如果你还没安装Elasticsearch&#xff0c;先执行docker安装Elasticsearch&#xff0c;下面是单机部署。 创建一个ES网络&#xff1a; docker n…

idea视图中顶部菜单栏找不到VCS

问题描述 idea视图中顶部菜单栏找不到VCS&#xff1a; 解决方案&#xff1a; 直接选择配置过git&#xff0c;此处操作&#xff1a; File->Settings->Version Control-> 点击 ->选择项目->VCS选择none ->点击apply -> 点击ok&#xff1a;

怎样修改库包的文件名?

python中&#xff0c;安装crypto模块时&#xff0c;会遇到crypto文件夹都是小写字母的情况&#xff0c;引用时又是首字母大写&#xff0c;这个时候&#xff0c;需要把库包文件名首字母改为大写字母。windows中一般的文件名命名中字母的大小写是不进行区分的&#xff0c;但是在p…

期权具体怎么交易详细的操作流程?

期权就是股票&#xff0c;唯一区别标的物上证指数&#xff0c;会看大盘吧&#xff0c;交易两个方向认购做多&#xff0c;认沽做空&#xff0c;双向t0交易&#xff0c;期权具体交易流程可以理解选择方向多和空&#xff0c;选开仓的合约&#xff0c;买入开仓和平仓没了&#xff0…

win10如何查看本机ip地址?三招搞定,快来试试吧

在数字化时代&#xff0c;IP地址作为网络设备的唯一标识&#xff0c;对于计算机使用者来说具有重要意义。无论是为了进行网络设置、远程连接&#xff0c;还是解决网络问题&#xff0c;了解如何查看本机IP地址都是一项必备技能。对于使用Windows 10操作系统的用户来说&#xff0…

Idea工具的使用技巧与常见问题解决方案

一、使用技巧 1、启动微服务配置 如上图&#xff0c;在编辑配置选项&#xff0c;将对应的启动入口类加进去&#xff0c; 增加jvm启动参数&#xff0c; 比如&#xff1a; -Denvuat 或者 -Denvuat -Dfile.encodingUTF-8 启动配置可能不是-Denvuat&#xff0c;这个自己看代…

媒体邀约资源报道(重庆邀约媒体)

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 重庆拥有电视台、广播电台、报纸、杂志以及网络媒体等多种丰富的媒体资源&#xff0c;企业和机构可以根据自身需求和活动特点选择合适的媒体进行邀约合作。以下是重庆地区的一些主要媒体…

全球十大体育赛事API服务

体育赛事API汇总&#xff1a; Broadage全球橄榄球赛事数据Broadage全球棒球赛事数据Broadage全球篮球实时数据Broadage全球冰球赛事数据Broadage全球排球实时数据TennisApi全球网球赛事讯息Broadage全球足球实时数据棒球数据【纳米数据】

SOLIDWORKS Toolbox根据需求灵活配置详解

用户在SOLIDWORKS中设计时&#xff0c;往往需要在零件中添加不同的孔特征、在装配体结构中添加不同的标准零件&#xff0c;Toolbox为用户提供了比较丰富的孔特征、标准零件选择&#xff0c;大大加快了研发工程师的设计效率;但是用户在使用Toolbox的时候仍会发现以下一些问题&am…

高熔体强度聚丙烯(HMSPP)属于高端聚丙烯 我国市场国产化进程有所加快

高熔体强度聚丙烯&#xff08;HMSPP&#xff09;属于高端聚丙烯 我国市场国产化进程有所加快 高熔体强度聚丙烯&#xff08;HMSPP&#xff09;又称高熔体强度PP&#xff0c;是一种含有微交联结构或长支链结构的改性聚丙烯。高熔体强度聚丙烯具有绿色环保、轻量化、结晶性好、熔…

查询一个网站的服务信息

1、网址 https://sitereport.netcraft.com/ 2、输入要查询的网站 3、点look up即可查看

扬腾创新golang2轮面试,二面相当硬核。我差点崩溃。。

一面 1、自我介绍&#xff0c;换工作的原因是什么&#xff1f; 2、物流开发平台是做什么&#xff1f;链路上都有哪些核心模块&#xff1f; 一个单下过来&#xff0c;分配给哪个3PL&#xff1f;有什么要求吗&#xff1f;是怎么设计的&#xff1f; 保证履约系统稳定性方面有做…