仿muduo的高并发服务器——前置知识讲解和时间轮模块

前面我们已经对这个项目进行了基本的模块分析拆解,本篇我们在开始前先了解一些前置知识并开始进行项目编写

相关的代码如下:仿muduo服务器: 本项目致力于实现一个仿造muduo库的简易并发服务器,为个人项目,参考即可

目录

前置知识

bind

timerfd

核心特性

优势

相关接口

timerfd_create

timerfd_settime

timerfd_gettime

[read 读取到期次数(非专用接口,但常用)](#read 读取到期次数(非专用接口,但常用))

示例

时间轮定时器

源码实现


前置知识

bind

cpp 复制代码
/*
    std::bind 是 C++11 引入的函数适配器,位于 <functional> 中。它将可调用对象与其部分参数绑定,
    生成一个新的可调用对象,支持参数占位符(_1, _2, ...)实现参数重排、延迟求值及部分应用
    它的简化表达式为:
    namespace std 
    {
        template< class F, class... BoundArgs >
        auto bind( F&& f, BoundArgs&&... bound_args );
    }
    其中 F 是可调用对象类型,BoundArgs 是要绑定的参数类型。bind 返回一个新的可调用对象,
    当调用该对象时,会将绑定的参数与占位符参数一起传递
    std::placeholders::_1, _2, ... 是占位符,用于指定调用时传入的参数位置。例如:
    auto f = std::bind(func, _1, 42); // 绑定 func
*/
#include <iostream>
#include <functional>

void add(int a, int b) { std::cout << a + b << '\n'; }

struct Mul 
{
    int operator()(int a, int b) const { return a * b; }
    int member(int x, int y) const { return x - y; }
};

int main() 
{
    using namespace std::placeholders;  // _1, _2...

    // 1. 固定第一个参数为 10,第二个由调用时指定
    auto add10 = std::bind(add, 10, _1);
    add10(5);  // 输出 15

    // 2. 参数重排:将 (a,b) -> (b,a)
    auto rev = std::bind(add, _2, _1);
    rev(3, 7); // 输出 10 (add(7,3))

    // 3. 绑定函数对象
    Mul mul;
    auto product = std::bind(mul, _1, _2);
    std::cout << product(4, 5) << '\n'; // 20

    // 4. 绑定成员函数:需传对象指针或引用
    auto sub = std::bind(&Mul::member, &mul, _1, _2);
    std::cout << sub(10, 3) << '\n';   // 7

    // 5. 结合 lambda(bind 通常不如 lambda 直观,但可统一旧接口)
    auto lam = std::bind([](int x, int y) { return x * y; }, 6, _1);
    std::cout << lam(7) << '\n';       // 42
}

timerfd

timerfdLinux 特有 的定时器接口,它将定时器事件以文件描述符的形式呈现,使得定时器可以像普通文件一样被 readselectpollepoll 等 I/O 多路复用机制管理

核心特性

  • 创建timerfd_create(clockid_t clockid, int flags)

    返回一个文件描述符,clockid 可选 CLOCK_REALTIMECLOCK_MONOTONIC 等,flags 可设 TFD_NONBLOCKTFD_CLOEXEC

  • 启动/设置timerfd_settime(int fd, int flags, const struct itimerspec *new_value, struct itimerspec *old_value)

    定义首次超时时间和后续间隔周期,flags=0 表示绝对时间,TFD_TIMER_ABSTIME 表示相对时间。

  • 读取事件 :当定时器到期,read(fd, buf, sizeof(uint64_t)) 会返回一个 uint64_t 整数,表示自上次读取后到期的次数(通常用于处理多次到期合并)。

  • 关闭close(fd)

优势

  • 统一事件源 :将定时器与 socket、信号等一起放入 epoll 循环,无需单独维护定时器列表。

  • 精确到期计数:可避免信号式定时器的异步安全问题。

相关接口

timerfd_create
  • 作用

    创建一个定时器对象,并将其与一个新的文件描述符关联。此后该定时器可通过文件描述符进行 I/O 监控(如 pollepoll),到期事件通过 read 读取。

  • 表达式

    cpp 复制代码
    int timerfd_create(int clockid, int flags);
  • 参数

    • clockid:指定定时器使用的时钟源。常用值:CLOCK_REALTIME(系统实时时钟,受系统时间调整影响)、CLOCK_MONOTONIC(单调递增时钟,不受系统时间变更影响)。

    • flags:修改行为。TFD_NONBLOCK 使描述符为非阻塞模式;TFD_CLOEXEC 在执行 exec 时自动关闭描述符。

  • 返回值

    成功时返回一个非负整数文件描述符;失败时返回 -1,并设置 errno 指示错误原因。

timerfd_settime
  • 作用

    启动或停止由 timerfd_create 创建的定时器,设置首次到期时间以及后续周期性间隔。

  • 表达式

    cpp 复制代码
    int timerfd_settime(int fd, int flags, 
            const struct itimerspec *new_value, 
            struct itimerspec *old_value);
  • 参数

    • fdtimerfd_create 返回的文件描述符。

    • flags:控制时间的解释方式。0 表示相对时间(new_value 中的时间相对于调用时刻);TFD_TIMER_ABSTIME 表示绝对时间(基于时钟的绝对点)。

    • new_value:指向 itimerspec 结构的指针,包含两个成员:it_value(首次到期时间)和 it_interval(后续周期间隔)。若 it_value 为零则停止定时器;若 it_interval 为零则为单次定时器。

    • old_value:非空时,返回定时器之前设置的剩余时间及间隔;可为 NULL 表示不关心。

  • 返回值

    成功返回 0;失败返回 -1,并设置 errno

timerfd_gettime
  • 作用

    查询定时器的当前状态,即距离下一次到期的剩余时间以及当前的周期间隔。

  • 表达式
    int timerfd_gettime(int fd, struct itimerspec *curr_value);

  • 参数

    • fd:定时器文件描述符。

    • curr_value:输出参数。调用成功后,curr_value->it_value 返回距离下一次到期的剩余时间(若定时器已停止则为零),curr_value->it_interval 返回定时器的周期间隔(与设置时相同)。

  • 返回值

    成功返回 0;失败返回 -1,并设置 errno

read 读取到期次数(非专用接口,但常用)
  • 作用

    从定时器描述符中读取到期次数。每次定时器到期,内核会增加一个内部计数器;read 会取出该计数器的当前值并清零。这允许程序在一次 read 中获知自上次读取以来到期的总次数,适用于处理积压事件。

  • 表达式

    cpp 复制代码
    ssize_t read(int fd, void *buf, size_t count);
  • 参数

    • fdtimerfd_create 返回的文件描述符。

    • buf:必须指向一个 uint64_t 类型的变量,用于接收到期计数。

    • count:必须为 sizeof(uint64_t)(即 8 字节),否则 read 可能失败或读取不完整。

  • 返回值

    成功时返回读取的字节数(通常为 8)。若定时器从未到期且描述符为非阻塞模式,read 会立即返回 -1 并设置 errnoEAGAINEWOULDBLOCK;若描述符为阻塞模式,则一直阻塞直到至少有一次到期。失败时返回 -1 并设置 errno

示例

cpp 复制代码
/*
    总体介绍:
    timerfd是Linux下特有的时间轮定时器,
    提供了一个文件描述符接口,可以通过read()系统调用来获取定时器的到期次数。
    它支持单次定时和周期性定时,可以设置初始延迟和间隔时间。
    timerfd的使用可以简化定时器的管理,避免了传统定时器中需要处理信号或线程同步的问题。


    常用结构体成员介绍:
    struct itimerspec 
    {
        struct timespec it_interval;  // 周期间隔
        struct timespec it_value;     // 首次到期时间
    };

    struct timespec 
    {
        time_t tv_sec;   // 秒
        long   tv_nsec;  // 纳秒(范围 0 ~ 999999999)
    };
    成员详解:

    it_value:指定定时器的首次到期时间。

        若 it_value.tv_sec 和 it_value.tv_nsec 均非零,定时器在该绝对/相对时间点首次触发。

        若两者均为零,定时器被停止( disarm)。

        若使用相对时间模式(flags=0),it_value 表示从调用时刻开始的时长。

        若使用绝对时间模式(TFD_TIMER_ABSTIME),it_value 表示基于时钟的绝对时间点。

    it_interval:指定定时器的周期间隔。

        若 it_interval 的任一成员非零,定时器在首次到期后,每隔该间隔重复触发。

        若 it_interval.tv_sec 和 it_interval.tv_nsec 均为零,定时器为单次模式,首次到期后即停止。

    使用注意:
    tv_nsec 的值必须小于 1e9(即 1 秒)。当设置绝对时间时,需注意 it_value 不能小于当前时间,否则可能立即到期或导致错误。
*/ 
#include <sys/timerfd.h>
#include <unistd.h>
#include <stdint.h>
#include <iostream>
#include <cstring>
#include <cerrno>

int main() 
{
    // 1. timerfd_create:创建定时器描述符
    int tfd = timerfd_create(CLOCK_MONOTONIC, 0);
    if (tfd == -1) 
    {
        std::cerr << "timerfd_create failed: " << strerror(errno) << std::endl;
        return 1;
    }

    // 配置定时器:首次 1 秒后到期,之后每隔 1 秒周期触发
    struct itimerspec new_value = {};
    new_value.it_value.tv_sec = 1;      // 首次到期时间:1 秒后
    new_value.it_value.tv_nsec = 0;
    new_value.it_interval.tv_sec = 1;   // 周期间隔:1 秒
    new_value.it_interval.tv_nsec = 0;

    // 2. timerfd_settime:启动定时器
    if (timerfd_settime(tfd, 0, &new_value, nullptr) == -1) 
    {
        std::cerr << "timerfd_settime failed: " << strerror(errno) << std::endl;
        close(tfd);
        return 1;
    }

    // 3. timerfd_gettime:获取定时器当前剩余时间和间隔
    struct itimerspec curr = {};
    if (timerfd_gettime(tfd, &curr) == -1) 
    {
        std::cerr << "timerfd_gettime failed: " << strerror(errno) << std::endl;
    } 
    else 
    {
        std::cout << "Initial remaining time: " << curr.it_value.tv_sec 
                  << " sec " << curr.it_value.tv_nsec << " ns" << std::endl;
        std::cout << "Interval: " << curr.it_interval.tv_sec 
                  << " sec " << curr.it_interval.tv_nsec << " ns" << std::endl;
    }

    // 循环读取 5 次到期事件
    for (int i = 0; i < 5; ++i) 
    {
        uint64_t exp = 0;
        // 4. read:阻塞读取到期次数
        ssize_t s = read(tfd, &exp, sizeof(exp));
        if (s != sizeof(exp)) 
        {
            std::cerr << "read error: " << strerror(errno) << std::endl;
            break;
        }
        std::cout << "Timer expired " << exp << " time(s)" << std::endl;

        // 再次调用 timerfd_gettime 查看剩余时间
        if (timerfd_gettime(tfd, &curr) == 0) 
        {
            std::cout << "  Next expiration in: " << curr.it_value.tv_sec 
                      << " sec " << curr.it_value.tv_nsec << " ns" << std::endl;
        }
    }

    // 5. close:关闭描述符,释放定时器
    close(tfd);
    return 0;
}

结果为:

时间轮定时器

在上述的代码中,存在⼀个很大的问题,每次超时都要将所有的连接遍历⼀遍,如果有上万个连接,效率无疑是较为低下的。

或许我们可以用优先级队列建立一个从大到小排序的堆,时间小的逐个释放,这样也是一个不错的思路。

这里我们介绍另一种思路:时间轮

思路讲解:如果我们定义了一个数组,并且有一个指针,指向数组起始位置,这个指针每秒钟向后走动一步,走到哪里,则代表哪里的任务该被执行了,那么如果我们想要定一个3s后的任务,则只需要将任务添加到tick+3位置,则每秒中走一步,三秒钟后tick走到对应位置,这时候执行对应位置的任务即可。

但是有以下几个情况需要解决:

  1. 同一时刻的定时任务只能添加一个,需要考虑如何在同一时刻支持添加多个定时任务

解决方案:将时间轮的一维数组设计为二维数组(时间轮一位数组的每一个节点也是一个数组)

  1. 假设当前的定时任务是一个连接的非活跃销毁任务,这个任务什么时候添加到时间轮中比较合适?

一个连接30s内都没有通信,则是一个非活跃连接,这时候就销毁

但是一个连接如果在建立的时候添加了一个30s后销毁的任务,但是这个连接30s内人家有数据通信,在第30s的时候不是一个非活跃连接。

思想 :需要在一个连接有IO事件产生的时候,延迟定时任务的执行

作为一个时间轮定时器,本身并不关注任务类型,只要是时间到了就需要被执行。

解决方案:类的析构函数 + 智能指针shared_ptr,通过这两个技术可以实现定时任务的延时

源码实现

timewheel.hpp

cpp 复制代码
#pragma once
#include <memory>
#include <cstdint>
#include <vector>
#include <functional>
#include <unordered_map>
namespace IMMUDUO
{   
    using Task=std::function<void()>;
    using Release=std::function<void()>;
    class TimeTask
    {
        
        public:
            TimeTask(uint64_t id,uint32_t timeout,const Task& task);
            ~TimeTask();
            //设置定时器任务对象被销毁时需要执行的任务
            void SetRelease(const Release& release);
            //获取定时器任务对象超时时间
            uint32_t GetTimeout()const;
            //获取定时器任务对象ID
            uint64_t GetId()const;
        private:
            uint64_t id_;       //定时器任务对象ID
            uint32_t timeout_;  //定时器任务超时时间
            Task task_;         //定时器需要执行的任务
            Release release_;   //定时器任务对象被销毁时需要执行的任务
    };
    class TimeWheel
    {
        using SharedTask=std::shared_ptr<TimeTask>;
        using WeakTask=std::weak_ptr<TimeTask>;
        void RemoveTimer(uint64_t id);
        public:
            TimeWheel();
            //添加定时器任务对象到时间轮中
            void TimerAdd(uint64_t id,uint32_t timeout,const Task& task);
            //刷新定时任务
            void TimerRefresh(uint64_t id);
            //执行定时任务
            void TimerRunTask();
            ~TimeWheel()=default;
        private:
            int ticks_;                                     //当前秒针,走到哪里执行哪里任务
            int capacity_;                                  //最大延迟时间
            std::vector<std::vector<SharedTask>>wheel_;    //时间轮,包含多个槽,每个槽中包含多个定时器任务对象
            std::unordered_map<uint64_t,WeakTask> taskMap_; //定时器任务对象ID与定时器任务对象的映射关系
    };
}

timewheel.cpp

cpp 复制代码
#include "timewheel.hpp"
namespace IMMUDUO
{
    TimeTask::TimeTask(uint64_t id,uint32_t timeout,const Task& task)
        :id_(id),timeout_(timeout),task_(task)
    {}
    TimeTask::~TimeTask()
    {
        task_();
        release_();
    }
    void TimeTask::SetRelease(const Release& release)
    {
        release_=release;
    }
    uint32_t TimeTask::GetTimeout()const
    {
        return timeout_;
    }
    uint64_t TimeTask::GetId()const
    {
        return id_;
    }
    TimeWheel::TimeWheel()//初始化列表顺序取决于类内成员声明的顺序
        :ticks_(0),capacity_(60),wheel_(capacity_)
    {}
    void TimeWheel::RemoveTimer(uint64_t id)
    {
        if(taskMap_.find(id)!=taskMap_.end())
        {
            taskMap_.erase(id);
        }
    }

    void TimeWheel::TimerAdd(uint64_t id,uint32_t timeout,const Task& task)
    {
        SharedTask taskPtr=std::make_shared<TimeTask>(id,timeout,task);
        //bind写法
        // taskPtr->SetRelease(std::bind(&TimeWheel::RemoveTimer,this,id));
        //推荐lambda表达式
        taskPtr->SetRelease([this,id](){
            this->RemoveTimer(id);
        });
        taskMap_[id]=WeakTask(taskPtr);
        int pos=(ticks_+timeout)%capacity_;
        wheel_[pos].push_back(taskPtr);
    }
    void TimeWheel::TimerRefresh(uint64_t id)
    {
        auto it=taskMap_.find(id);
        //通过定时器的weak_ptr获取shared_ptr的定时器任务对象
        if(it==taskMap_.end())
        {
            return ;//没找到刷新任务,无法刷新,无法延迟
        }
        SharedTask taskPtr=it->second.lock();//获取对应的shared_ptr的定时器任务对象
        if(!taskPtr)
        {
            return ;//weak_ptr已经失效,无法刷新
        }
        int dlay=taskPtr->GetTimeout();
        int pos=(ticks_+dlay)%capacity_;
        wheel_[pos].push_back(taskPtr);
    }
    void TimeWheel::TimerRunTask()
    {
        ticks_=(ticks_+1)%capacity_;
        wheel_[ticks_].clear();//走到哪里释放哪里的任务对象
    }
}

本期内容到这里就结束了,喜欢请点个赞谢谢

封面图自取:

相关推荐
花间相见2 小时前
【MS-Swift实战】:LoRA原理+核心参数(r/alpha)调参指南(适配Qwen-1.8B医疗场景)
开发语言·r语言·swift
小江的记录本2 小时前
【分布式】分布式核心组件——分布式限流:固定窗口、滑动窗口、漏桶、令牌桶算法,网关层/服务层限流实现
java·分布式·后端·python·算法·安全·面试
OYangxf2 小时前
C++中的回调函数:从函数指针到现代可调用对象
c++
Hanson,2 小时前
SpringBoot前后端分离框架中,在请求头加入签名
java·spring boot·后端
求知也求真佳2 小时前
S03|待办写入:让 AI 不再走一步忘一步,多步任务不再跑偏
开发语言·agent
九转成圣2 小时前
Spring Boot 导出 Excel 最佳实践:从 POI 函数式封装到 EasyExcel 的“降维打击”
spring boot·后端·excel
Metaphor6922 小时前
使用 Python 提取 PDF 文件中的文本、表格、图片
开发语言·python·pdf
小夏子_riotous2 小时前
Docker学习路径——5、容器数据卷
linux·运维·服务器·学习·docker·容器·云计算
liyi_hz20082 小时前
O2OA(翱途) V10 升级说明(三)数据中心:精准查询·严谨权限·优质视图
后端·java-ee·开源软件