muduo的EventLoop.runAfter()的一次错误使用记录

本文记录了一次在使用muduo的EventLoop.runAfter()时遇到的问题,深入探讨了由于线程sleep导致定时任务延迟的原因,并对比了boost.asio的解决方案,最终找到了正确的使用方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

muduo的EventLoop.runAfter()的一次错误使用记录

问题出现

项目中使用loop.runAfter(60, …)设置一个定时任务,但是发现预期的效果没有出现,总是感觉定时任务调用时已经过了60秒,很奇怪,加了打印,发现:

loop.runAfter(60, ...), 实际调用时已经过了107~108秒
loop.runAfter(90, ...), 实际调用时已经过了154~158秒

EventLoop一直在使用,之前没有遇到过这种问题啊。反复使用后就是那样。

寻找其他做法

因为项目第二天就要提测,比较急,我就想能否有其他的代替方案,首先想到了boost.asio,这个东西之前用过,在有了muduo之后就没怎么用了。

利用boost::asio::io_service和boost::asio::deadline_timer实现runAfter应该没什么问题(自己还是太年轻,呵呵~),写了几个简单的例子,好像还真的可以。

试试加入多个任务,t_.expires_from_now(boost::posix_time::seconds(10));t_.async_wait(boost::bind(&Test::fun, this));,问题出现了,boost::asio::deadline_timer中async_wait的任务总是立刻返回之前的任务,什么原因?

网上查了才发现自己对boost::asio::deadline_timer理解错了。boost::asio::deadline_timer

  1. cancel的使用async_wait设定的handler会被调用, 超时处理函数应该区分 boost::asio::error::operation_aborted
  2. 多次调用async_wait设定多个handler,则超时的时候它们都会被调用,如果将同一个handler设定多次,它也会被调用多次
  3. 在timer未超时前又调用expires_from_now相当于调用cancel
  4. 超时时间重新设定后,必须async_wait重新设定handler

总的来说一句话,boost::asio::deadline_timer只能维护一个定时任务,如果再加入定时任务,之前的就立刻返回了。

看来这条路走不通。

回到原点,解决问题

人在走投无路的时候才会孤注一掷,没有了其他的花花肠子,只能重新来看EventLoop.runAfter()出错的原因。
自己写了一个小例子:

	#include <muduo/net/EventLoop.h>
	#include <muduo/net/EventLoopThread.h>
	void fun() {
	    std::cout << "fun-" << time(NULL) << std::endl;
	}
	int main(int argc, char ** argv){
	    muduo::net::EventLoop loop;
	    std::cout << "main1-" << time(NULL) << std::endl;
	    loop.runAfter(20, fun);
	    loop.loop();
	    return 0;
	}

20秒之后fun()被调用,打印日志没有问题啊,问题出在哪里呢?
既然这么调用没有问题,说明muduo没毛病啊,只能是我自己使用错了。

继续看看项目,我的项目中,loop干4件事,3个runAfter和1个runEvery,3个runAfter都有问题,runEvery却没有问题(我看了日志)。想想这次使用和上次使用的区别,没什么啊,不过就是runEvery有几个sleep(),不会是这个引起的吧?(曾经多少次想到这,都没有注意),现在也没有其他的办法,就写个例子测试一下吧:

	#include <muduo/net/EventLoop.h>
	#include <muduo/net/EventLoopThread.h>
	void fun() {
	    std::cout << "fun-" << time(NULL) << std::endl;
	}
	void fun2() {
	    std::cout << "fun2-1" << time(NULL) << std::endl;
	    sleep(30);
	    std::cout << "fun2-2" << time(NULL) << std::endl;
	}
	
	int main(int argc, char ** argv){
	    muduo::net::EventLoop loop;
	    std::cout << "main1-" << time(NULL) << std::endl;
	    loop.runAfter(20, fun);
	    
	    std::cout << "main2-" << time(NULL) << std::endl;
	    loop.runAfter(10, fun2);
	
	    loop.loop();
	    return 0;
	}

10秒以后没有输出,30秒以后全部输出。我懂了,原来是sleep搞的鬼。对啊,loop本质上就是一个线程,定时任务到期了,线程却在sleep,当然不能回调(脑子一下通了,之前肯定塞满了浆糊)。

那就是3个runAfter和另1个runEvery不能用同一个loop,runEvery中的sleep会影响runAfter的调用,使用两个loop。

项目中修改,编译,测试,没毛病。。。

教训和反思

  1. 不能放过一点蛛丝马迹
  2. 多想多测试
<think>好的,用户想了解muduo库中EventLoop是由哪个线程启动的,以及启动流程是怎样的。首先我需要回忆muduo的基本结构。记得muduo是基于Reactor模式,每个EventLoop对应一个IO线程,遵循one loop per thread的设计。 接下来,用户的问题核心在于EventLoop的启动线程和流程。首先,EventLoop通常由它所属的线程启动,也就是创建EventLoop对象的线程。需要确认的是,EventLoop构造函数中是否绑定了当前线程,这样在启动时就能确保在同一个线程运行。 然后启动流程方面,EventLoop的启动应该包括初始化事件循环,比如创建epoll实例,定时器队列,以及准备事件处理的相关结构。当调用loop()方法时,会进入循环,处理就绪的事件,执行回调函数,直到退出条件满足。 可能用户在使用时遇到了线程相关的问题,或者想知道如何正确初始化和运行EventLoop。需要强调EventLoop的生命周期管理和线程安全,比如确保loop()在正确的线程调用,避免多线程竞争。 另外,应该提到EventLoop::loop()的工作流程,包括epoll_wait等待事件,处理活跃事件,执行pending任务,处理定时器超时等步骤。可能还需要解释如何通过其他线程添加任务到EventLoop,比如runInLoop函数通过唤醒机制确保任务在所属线程执行。 用户可能的实际需求是想要在自己的项目中正确使用muduoEventLoop,或者调试启动顺序的问题。因此,回答时需要明确线程绑定和启动步骤,帮助用户理解如何正确初始化和运行EventLoop,避免跨线程调用的问题。 需要检查是否有遗漏的关键点,比如EventLoop的构造函数中检查是否已经存在其他EventLoop在该线程,确保单例。还有loop()的退出条件,比如调用quit()函数,这些可能影响启动流程的持续性。 总结回答的结构:先说明启动线程是创建EventLoop的线程,然后分步骤描述启动流程,强调线程安全和使用注意事项。确保用户能清晰理解EventLoop的初始化和运行机制。</think>**muduoEventLoop 由哪个线程启动?** EventLoop 的启动线程是 **创建它的线程**,即 `EventLoop` 对象构造时所在的线程。muduo 遵循 "one loop per thread" 设计原则,每个线程最多拥有一个 EventLoop,且其生命周期必须与线程绑定,确保事件循环的线程安全性。 --- **EventLoop 的启动流程** 1. **构造 EventLoop 对象** - 在某个线程中构造 `EventLoop` 对象时,构造函数会通过 `ThreadIdCache` 记录当前线程 ID,并检查该线程是否已存在其他 EventLoop(确保单例)。 - 初始化关键组件: - 创建 `Poller`(默认用 `epoll`)用于监听文件描述符事件。 - 创建 `TimerQueue` 管理定时任务。 - 创建 `wakeupFd`(通过 `eventfd` 实现)用于线程间唤醒。 2. **调用 loop() 方法** 在构造 EventLoop 的线程中显式调用 `loop()`,进入事件循环: ```cpp EventLoop loop; loop.loop(); // 启动事件循环 ``` 3. **事件循环核心逻辑** - **步骤 1:处理待执行任务** 通过 `doPendingFunctors()` 执行其他线程通过 `runInLoop()` 提交的回调任务(例如跨线程添加定时器)。 - **步骤 2:等待事件就绪** 调用 `Poller::poll()` 阻塞等待 `epoll_wait` 返回就绪的 I/O 事件或定时器超时。 - **步骤 3:处理活跃事件** 遍历就绪的 `Channel` 列表,调用对应的事件处理函数(如处理新连接、读写事件)。 - **步骤 4:处理定时任务** 通过 `TimerQueue` 处理到期的定时器回调。 - **循环条件** 重复上述步骤,直到 `quit_` 标志被置为 `true`(例如调用 `quit()` 退出循环)。 --- **关键设计要点** 1. **线程绑定** `EventLoop` 的 `loop()` 和 `quit()` 必须在所属线程调用,跨线程操作需通过 `runInLoop()` 提交任务,由 `wakeupFd` 唤醒事件循环执行。 2. **唤醒机制** 通过向 `wakeupFd` 写入数据,打破 `epoll_wait` 的阻塞,确保及时处理跨线程任务。 3. **资源管理** `EventLoop` 析构时自动释放 `Poller`、`TimerQueue` 等资源,通常在主线程中管理生命周期。 --- **示例代码片段** ```cpp // 在 IO 线程中构造并启动 EventLoop EventLoop loop; // 在另一个线程提交任务到 EventLoop std::thread other_thread([&loop] { loop.runInLoop([] { std::cout << "This runs in EventLoop's thread!\n"; }); }); loop.loop(); // 阻塞,直到 quit() 被调用 ``` 通过理解线程绑定与事件循环流程,可以避免多线程环境下的竞态问题,正确利用 muduo 的高效事件驱动模型。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值