网易面试:手撕定时器

简介: 网易面试:手撕定时器

概述:

本文使用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

目录
相关文章
|
6月前
|
Oracle Java 关系型数据库
一次惨痛的面试:“网易提前批,我被虚拟线程问倒了”
【5月更文挑战第13天】一次惨痛的面试:“网易提前批,我被虚拟线程问倒了”
102 4
|
5月前
|
设计模式 NoSQL Java
网易面试:SpringBoot如何开启虚拟线程?
虚拟线程(Virtual Thread)也称协程或纤程,是一种轻量级的线程实现,与传统的线程以及操作系统级别的线程(也称为平台线程)相比,它的创建开销更小、资源利用率更高,是 Java 并发编程领域的一项重要创新。 > PS:虚拟线程正式发布于 Java 长期支持版(Long Term Suort,LTS)Java 21(也就是 JDK 21)。 虚拟线程是一种在 Java 虚拟机(JVM)层面实现的逻辑线程,不直接和操作系统的物理线程一一对应,因此它可以减少上下文切换所带来的性能开销。 操作系统线程、普通线程(Java 线程)和虚拟线程的关系如下: ![image.png](https:
69 0
网易面试:SpringBoot如何开启虚拟线程?
|
6月前
|
前端开发 Java 物联网
Android开发面试基础,3天拿到网易Android岗offer
Android开发面试基础,3天拿到网易Android岗offer
|
6月前
|
设计模式 前端开发 网络协议
Android 开发网易面试凉凉经,面试官:基础不牢,彻底帮你搞懂
Android 开发网易面试凉凉经,面试官:基础不牢,彻底帮你搞懂
|
6月前
|
机器学习/深度学习 Java 数据挖掘
selenium的配置与基本使用(1),2024年最新网易Python面试必问
selenium的配置与基本使用(1),2024年最新网易Python面试必问
|
6月前
|
前端开发 JavaScript
JavaScript新科技:PostCSS的安装和使用,2024年最新2024网易Web前端高级面试题总结
JavaScript新科技:PostCSS的安装和使用,2024年最新2024网易Web前端高级面试题总结
|
6月前
|
Android开发
Android热补丁动态修复实践,腾讯&字节&网易&华为Android面试题分享
Android热补丁动态修复实践,腾讯&字节&网易&华为Android面试题分享
|
6月前
|
NoSQL Java 关系型数据库
分享面试阿里、京东、网易等大厂后的面经及面试心得—远程面试
受疫情影响,阿里、百度、网易等互联网企业都开启了远程面试。 那么远程面试和正常面试有什么不同吗?并没有! 企业招聘的要求没有改变,改变的仅仅是面试的地点。今年远程面试完几家互联网企业(阿里、京东、网易、头条),总结下来面试的大体思路都基本一致。比如:
|
消息中间件 算法 Java
面试造飞机? 网易在职顶级大佬“java面试真题 2023” (助上岸)
现在的互联网环境可以说是比较难受的了,学习it的越来越多行业越来越卷,导致更加多的程序员去争取更少的岗位。其实很多人的技术还是不错的但一面试可能还是会被刷下去。
96 0
|
消息中间件 缓存 NoSQL
4 年 Java 经验,阿里网易拼多多面试总结、心得体会
由于个人发展的原因和工作上的变动,产生了想出来看看机会的想法。经过了一段时间的准备,5 月下旬开始出来面试,面到了 7 月上旬,如愿拿到了自己心仪公司的 offer。按照自己的习惯,将这次面试过程中的一些经验总结、心得体会记录下来,自己留个记录,也希望可以帮助到一些同学。
445 0