muduo网络库核心代码阅读(TimerId,Timer,TimerQueue)(7)

news/2025/2/22 17:46:40/文章来源:https://www.cnblogs.com/xiaodao0036/p/18731095

muduo提供了基于底层事件循环的定时器,通过底层提供的特殊文件描述符timerfd实现。TimerId、Timer和TimerQueue是定时器的主要实现类。

timerfd

timerfd是linux提供的基于文件描述符的定时器接口。他通过文件描述符与定时器关联,当定时器到期时,文件描述符会变为可读。因此可以与常用的IO多路复用机制(epoll、poll、select)配合使用,自实现定时器。

timerfd有三个主要方法:

// 创建一个timerfd
// @param clockid 时钟类型,可以是CLOCK_REALTIME或CLOCK_MONOTONIC
// @param flags 附加选项
// @return timerfd的文件描述符
int timerfd_create(int clockid, int flags);// 设置timerfd的超时时间
// @param fd timerfd的文件描述符
// @param flags 附加选项
// @param new_value 新的超时时间
// @param old_value 旧的超时时间
// @return 0表示成功,其他表示失败
int timerfd_settime(int fd, int flags, const struct itimerspec *new_value, struct itimerspec *old_value);// 获取当前timerfd设置的时间消息
// @param fd timerfd的文件描述符
// @param curr_value 当前的超时时间
int timerfd_gettime(int fd, struct itimerspec *curr_value);

itimerspec是一个存储时间信息的结构体,包含定时器到期时间和定时器重复触发的间隔时间(若为零,则表示只触发一次)。

TimerId、Timer和TimerQueue

muduo库定时器设计中,每个事件循环即EventLoop只拥有一个timerfd,注册某个事件循环上的所有定时任务都使用同一个timerfd。定时器队列TimerQueue就是注册在事件循环上的所有定时任务的管理类,同样每个事件循环只拥有一个TimerQueue。定时器类Timer则封装了每个定时任务的相关信息,包括超时时间、定时任务的回调函数等。TimerId的是每个定时任务的标识符,用户在往事件循环注册定时任务时,会返回对应任务的TimerId,以便后续管理。

TimerId

TimerId是定时器标识符,结构简单,只有两个成员变量没有其它成员函数:

Timer* timer_;      // 指向Timer对象
int64_t sequence_;      // 定时器序号,确保每个定时器唯一

Timer

Timer是定时器的具体实现类,封装了定时器的相关信息,包括超时时间、定时任务的回调函数等。

class Timer : noncopyable
{
public:// 构造函数// @param cb 定时任务的回调函数// @param when 定时器的到期时间// @param interval 定时器的重复触发间隔,若为零,则表示只触发一次Timer(TimerCallback cb, Timestamp when, double interval): callback_(std::move(cb)),expiration_(when),interval_(interval),repeat_(interval > 0.0),      // 时间间隔大于零,表示重复触发sequence_(s_numCreated_.incrementAndGet())    // 获取定时器序号,s_numCreated_是一个原子计数器{ }// 运行定时任务void run() const{callback_();}Timestamp expiration() const  { return expiration_; }bool repeat() const { return repeat_; }int64_t sequence() const { return sequence_; }// 重启定时器// @param now 当前时间void restart(Timestamp now);static int64_t numCreated() { return s_numCreated_.get(); }private:const TimerCallback callback_;  // 定时任务的回调函数Timestamp expiration_;       // 定时器的到期时间const double interval_;     // 定时器的重复触发间隔const bool repeat_;         // 是否重复触发const int64_t sequence_;    // 定时器序号static AtomicInt64 s_numCreated_;
};//实现
void Timer::restart(Timestamp now)
{if (repeat_){// 计算下次到期时间expiration_ = addTime(now, interval_);}else{expiration_ = Timestamp::invalid();}
}

TimerQueue

TimerQueue是定时器队列的实现类,管理注册在事件循环上的所有定时任务。

class TimerQueue : noncopyable
{
public:// 构造函数// @param loop 队列所在的事件循环explicit TimerQueue(EventLoop* loop);~TimerQueue();//向队列中添加一个定时器// @param cb 定时任务的回调函数// @param when 定时器的到期时间// @param interval 定时器的重复触发间隔,若为零,则表示只触发一次TimerId addTimer(TimerCallback cb,Timestamp when,double interval);// 取消一个定时任务// @param timerId 要取消的定时器的标识符void cancel(TimerId timerId);private:typedef std::pair<Timestamp, Timer*> Entry;typedef std::set<Entry> TimerList;      typedef std::pair<Timer*, int64_t> ActiveTimer;typedef std::set<ActiveTimer> ActiveTimerSet;// 在事件循环线程中添加一个定时器void addTimerInLoop(Timer* timer);// 在事件循环线程中取消一个定时器void cancelInLoop(TimerId timerId);// 处理timerfd可读事件void handleRead();// 获取已到期的定时器// @param now 文件描述符到期时间// @return 已到期的定时器列表std::vector<Entry> getExpired(Timestamp now);// 重置已到期的定时器void reset(const std::vector<Entry>& expired, Timestamp now);// 插入一个定时器bool insert(Timer* timer);EventLoop* loop_;     // 队列所在的事件循环const int timerfd_;      // timerfd的文件描述符Channel timerfdChannel_;        // timerfd对应的Channel,用于监听可读事件TimerList timers_;      // 定时器队列ActiveTimerSet activeTimers_;   // 当前活跃的定时器bool callingExpiredTimers_; /* atomic */    // 是否正在调用过期定时器的回调函数ActiveTimerSet cancelingTimers_;    // 待取消的定时器
};

std::set底层通过红黑树实现,是一个有序的集合,因此对于TimerList,若向其中插入定时器,则会自动根据std::pair的第一个参数(重载了<运算符的Timestamp)来自动排序(从小到大)。

添加定时器

外部调用addTimer()来向定时队列添加定时器。首先构造一个定时器对象,然后在事件循环线程中调用addTimerInLoop()来添加定时器。避免在多个线程中竞争资源

TimerId TimerQueue::addTimer(TimerCallback cb,Timestamp when,double interval)
{Timer* timer = new Timer(std::move(cb), when, interval);loop_->runInLoop(std::bind(&TimerQueue::addTimerInLoop, this, timer));// 返回构造的定时器的标识符return TimerId(timer, timer->sequence());
}

调用insert函数将定时器插入到定时器队列,并根据当前定时器的到期时间是否是最早到期的,来选择是否重置timerfd的到期时间。

void TimerQueue::addTimerInLoop(Timer* timer)
{loop_->assertInLoopThread();bool earliestChanged = insert(timer);if (earliestChanged){resetTimerfd(timerfd_, timer->expiration());}
}

插入定时器到timers_和activeTimers_中,并判断是否是最早到期的定时器

bool TimerQueue::insert(Timer* timer)
{loop_->assertInLoopThread();assert(timers_.size() == activeTimers_.size());bool earliestChanged = false;Timestamp when = timer->expiration();TimerList::iterator it = timers_.begin();if (it == timers_.end() || when < it->first){earliestChanged = true;}{std::pair<TimerList::iterator, bool> result= timers_.insert(Entry(when, timer));assert(result.second); (void)result;}{std::pair<ActiveTimerSet::iterator, bool> result= activeTimers_.insert(ActiveTimer(timer, timer->sequence()));assert(result.second); (void)result;}assert(timers_.size() == activeTimers_.size());return earliestChanged;
}
定时任务到期

当timerfd设置的时间到期,timerfd可读,事件循环捕获到该可读事件发生后会调用先前注册的该函数。

void TimerQueue::handleRead()
{loop_->assertInLoopThread();Timestamp now(Timestamp::now());    // 获取当前时间readTimerfd(timerfd_, now);     // 读取timerfd中的数据// 获取到期的定时器std::vector<Entry> expired = getExpired(now);// 标志当前正在处理到期的定时器任务callingExpiredTimers_ = true;// 清空待取消的定时器(后续讨论)cancelingTimers_.clear();// 处理任务for (const Entry& it : expired){it.second->run();}// 标志处理完成callingExpiredTimers_ = false;// 重置已到期的定时器reset(expired, now);
}

根据传入的时间获取当前到期的定时器,该函数通过整体批量处理的方式来提高效率。

std::vector<TimerQueue::Entry> TimerQueue::getExpired(Timestamp now)
{assert(timers_.size() == activeTimers_.size());std::vector<Entry> expired;// 构造边界元素,用于分割timers_Entry sentry(now, reinterpret_cast<Timer*>(UINTPTR_MAX));TimerList::iterator end = timers_.lower_bound(sentry);assert(end == timers_.end() || now < end->first);std::copy(timers_.begin(), end, back_inserter(expired));// 处理边界元素,从timers_中删除到期的定时器timers_.erase(timers_.begin(), end);for (const Entry& it : expired){// 从activeTimers_中删除已到期的定时器ActiveTimer timer(it.second, it.second->sequence());size_t n = activeTimers_.erase(timer);assert(n == 1); (void)n;}assert(timers_.size() == activeTimers_.size());return expired;
}

重置到期的定时任务,若定时器设置为重复触发则重新添加到timers_中,否则释放构造的Timer对象。最后重置timerfd的到期时间。

void TimerQueue::reset(const std::vector<Entry>& expired, Timestamp now)
{Timestamp nextExpire;for (const Entry& it : expired){ActiveTimer timer(it.second, it.second->sequence());// 如果定时器标志为重复触发且不是待取消的定时器if (it.second->repeat()&& cancelingTimers_.find(timer) == cancelingTimers_.end()){it.second->restart(now);insert(it.second);}else{delete it.second; }}if (!timers_.empty()){nextExpire = timers_.begin()->second->expiration();}if (nextExpire.valid()){resetTimerfd(timerfd_, nextExpire);}
}
取消定时器

同addTimer,提供外部调用接口,真正取消是在事件循环线程中

void TimerQueue::cancel(TimerId timerId)
{loop_->runInLoop(std::bind(&TimerQueue::cancelInLoop, this, timerId));
}

根据timerId定位到对应的定时器,并将其从activeTimers_和timers_中删除。如果删除时正在处理定时器任务,则将其放入cancelingTimers_中,在处理完毕后调用reset时处理。

void TimerQueue::cancelInLoop(TimerId timerId)
{loop_->assertInLoopThread();assert(timers_.size() == activeTimers_.size());ActiveTimer timer(timerId.timer_, timerId.sequence_);ActiveTimerSet::iterator it = activeTimers_.find(timer);if (it != activeTimers_.end()){size_t n = timers_.erase(Entry(it->first->expiration(), it->first));assert(n == 1); (void)n;delete it->first; activeTimers_.erase(it);}else if (callingExpiredTimers_){cancelingTimers_.insert(timer);}assert(timers_.size() == activeTimers_.size());
}

可能会有人不理解为什么要用两个容器来装定时器(TimerList和ActiveTimerSet)。我的理解是,TimerList装的是<Timestamp /*到期时间*/, Timer* /*定时器指针*/>,这样方便通过Timestamp来将所有定时器根据到期时间排序,更好获取到期的定时器以及设置timerfd的到期时间。而ActiveTimerSet装的是<Timer* /*定时器指针*/, int64_t /*定时器序号*/>,这和定时器标识符TimerId是一一对应的,方便在取消的时候,根据TimerId来定位到对应的定时器。

最后源码中在两次删除Timer对象时,注释有标注不要删除,而是放入一个待删除队列中,或许是考虑到直接删除容易引发错误,对指针管理不够严谨。因此我也建议大家在使用前改动一下。

定时器使用

EventLoop类提供了对应接口:

// 在time时刻运行cb
TimerId EventLoop::runAt(Timestamp time, TimerCallback cb)
{return timerQueue_->addTimer(std::move(cb), time, 0.0);
}// 在delay时长后运行cb
TimerId EventLoop::runAfter(double delay, TimerCallback cb)
{Timestamp time(addTime(Timestamp::now(), delay));return runAt(time, std::move(cb));
}// 每隔interval时长运行cb
TimerId EventLoop::runEvery(double interval, TimerCallback cb)
{Timestamp time(addTime(Timestamp::now(), interval));return timerQueue_->addTimer(std::move(cb), time, interval);
}// 取消定时器
void EventLoop::cancel(TimerId timerId)
{return timerQueue_->cancel(timerId);
}

示例:

void onTimeout() {std::cout << "Timeout!" << std::endl;
}int main() {EventLoop loop;// 添加一个 2 秒后执行的一次性定时任务TimerId timerId = loop.runAfter(2.0, onTimeout);// 取消定时任务loop.cancel(timerId);loop.loop(); // 进入事件循环return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

复制浏览器网页文字 粘贴时却乱码的解决

本文介绍在复制网页内容后粘贴时,粘贴内容出现一个方框图案而不是当初复制内容的解决办法~本文介绍在复制网页内容后粘贴时,粘贴内容出现一个方框图案而不是当初复制内容的解决办法。最近,需要将谷歌地球引擎(Google Earth Engine,GEE)网页中的一段代码复制到另一个网页中…

【翻译】凝视深渊:千核并发控制的评估

凝视深渊:千核并发控制的评估 作者 Xiangyao Yu MIT CSAIL yxy@csail.mit.edu George Bezerra MIT CSAIL gbezerra@csail.mit.edu Andrew Pavlo 卡内基梅隆大学 pavlo@cs.cmu.edu Srinivas Devadas MIT CSAIL devadas@csail.mit.edu Michael Stonebraker MIT CSAIL stonebrake…

2.21课堂测验

需求描述: 请设计一个仓储管理系统原型系统,该系统支持多个仓库的设立。统一设立物资台账,物资台账需包含物资编码、物资名称、规格、材质、供应商、品牌、物资分类,用户可以自定义物资的物资分类。需限制不同的物资名称、规格、材质的物资不能设立相同的物资编码。仓库人员…

CSS2

盒子模型所有HTML元素可以看作盒子,在CSS中,"box model"这一术语是用来设计和布局时使用 CSS盒模型本质上是一个盒子,封装周围的HTML元素,它包括:边距,边框,填充,和实际内容 Margin(外边距):元素与其他元素的距离(边框以外的距离),外边距是透明的,需要修…

清华大学第三版《DeepSeek:如何抓住DeepSeek红利》,普通人必备的Ai手册(附PDF手册)

前两天,清华大学发布了一部极为详尽的电子书——《DeepSeek从入门到精通》。这本书将DeepSeek的基础知识以及提示词的运用方法,讲解得清晰明了、浅显易懂,一经问世便迅速在网络上引发了广泛关注与热潮。 【清华第一版】《DeepSeek从入门到精通》率先登场,给广大对DeepSeek感…

Linux 中xargs 命令的-i和-I用法差异

001、-i 选项[root@PC1 dir1]# ls a.txt b.txt dir1 h.csv i.csv [root@PC1 dir1]# tree . ├── a.txt ├── b.txt ├── dir1 ├── h.csv └── i.csv1 directory, 4 files [root@PC1 dir1]# find *.txt | xargs -i mv {} dir1/ ## -i选项可以将xargs命令传…

宜家 App 存在的 bug All In One

宜家 App 存在的 bug All In One 某商品下架后,App 的订单中就无法再查看有关商品的任何详细信息宜家 App 存在的 bug All In One bugs某商品下架后,App 的订单中就无法再查看有关商品的任何详细信息解决方案使用 Google 搜索,获取对应的商品的网页版信息https://www.ikea.c…

2025寒假总结2

前言 这是第二篇总结,考虑到与前一篇的时间临近,所以不展开叙述做过的事情,此篇文章重点写关于最近的收获、现在的知识体系以及后面的计划。 Part 1 记录 这部分大概讲一下寒假做过的事,大体按照时间线展开。 首先 16 号是竞赛生大会,晚上回来后补完北京的游记,看了一会数…

为 Power Automate 注册 Adobe PDF Services

前言最近,再测试如何将HTML转换成PDF,然后发现Adobe有一个免费的操作可以用,好开心,赶紧注册一下。正文1.先注册一个账号,然后登录到Adobe Developer注册链接:https://www.adobe.com/go/getstarted_powerautomate2.这就是新建好的,然后密码要获取一下,如下图:3.然后,…

记一次golang项目context引发的进程OOM故障

之前写过一篇一种基于etcd实践节点自动故障转移的思路, 程序经历过一次线上进程内存持续上涨终OOOM的小事故, 本次技术复盘导致本次内存泄露的完整起因。 提炼代码: 业务函数etcdWatchLoop: 基于etcd的Watch机制持续监听/foo前缀键值对的变更; 收到Watch信道的变更消息,就…

记一次golang项目context引发的OOM故障

之前写过一篇一种基于etcd实践节点自动故障转移的思路, 程序经历过一次线上进程内存持续上涨终OOOM的小事故, 本次技术复盘导致本次内存泄露的完整起因。 提炼代码: 业务函数etcdWatchLoop: 基于etcd的Watch机制持续监听/foo前缀键值对的变更; 收到Watch信道的变更消息,就…