Muduo网络库源码分析(三)线程间使用eventfd通信和EventLoop::runInLoop系列函数

本文深入探讨了EventLoop的工作原理及其如何利用EventFD实现线程间高效通信,通过示例代码展示其在异步IO场景下的应用,包括EventLoop的启动、线程间数据交换及事件通知机制。

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

先说第一点,线程(进程)间通信有很多种方式(pipe,socketpair),为什么这里选择eventfd?


eventfd 是一个比 pipe 更高效的线程间事件通知机制,一方面它比 pipe 少用一个 file descripor,节省了资源;另一方面,eventfd 的缓冲区管理也简单得多,全部“buffer” 只有定长8 bytes,不像 pipe 那样可能有不定长的真正 buffer。

最重要的一点:当我们想要编写并发型服务器的时候,eventfd 可以完美取代 pipe去通知(唤醒)其他的进程(线程)。比如经典的异步IO reactor/selector 应用场景,去唤醒select的调用。可以和事件通知机制完美的的结合。


(一)eventfd

#include <sys/eventfd.h>
int eventfd(unsigned int initval, intflags);


简单的应用示例:
#include <sys/eventfd.h>  
#include <unistd.h>  
#include <stdlib.h>  
#include <stdio.h>  
#include <stdint.h>             /* Definition of uint64_t */  
  
#define handle_error(msg) \  
   do { perror(msg); exit(EXIT_FAILURE); } while (0)  
  
int  
main(int argc, char *argv[])  
{  
   uint64_t u;  
      
   int efd = eventfd(10, 0);  
   if (efd == -1)  
       handle_error("eventfd");  
     
   int ret = fork();  
   if(ret == 0)  
   {  
       for (int j = 1; j < argc; j++) {  
           printf("Child writing %s to efd\n", argv[j]);  
           u = atoll(argv[j]);  
           ssize_t s = write(efd, &u, sizeof(uint64_t));  
           if (s != sizeof(uint64_t))  
               handle_error("write");  
       }  
       printf("Child completed write loop\n");  
  
       exit(EXIT_SUCCESS);  
   }  
   else  
   {  
       sleep(2);  
  
       ssize_t s = read(efd, &u, sizeof(uint64_t));  
       if (s != sizeof(uint64_t))  
           handle_error("read");  
       printf("Parent read %llu from efd\n",(unsigned long long)u);  
       exit(EXIT_SUCCESS);  
   }  
}  

(二)EventLoop::loop、runInLoop、queueInLoop、doPendingFunctors


先看一下这四个函数总体的流程图:



依次解释:

// 该函数可以跨线程调用
void EventLoop::quit()
{
    quit_ = true;
    if (!isInLoopThread())
    {
        wakeup();
    }
}

//使用eventfd唤醒
void EventLoop::wakeup()
{
  uint64_t one = 1;
  //ssize_t n = sockets::write(wakeupFd_, &one, sizeof one);
  ssize_t n = ::write(wakeupFd_, &one, sizeof one);
  if (n != sizeof one)
  {
    LOG_ERROR << "EventLoop::wakeup() writes " << n << " bytes instead of 8";
  }
}
如果不是当前IO线程调用quit,则需要唤醒(wakeup())当前IO线程,因为它可能还阻塞在poll的位置(EventLoop::loop()),这样再次循环判断 while (!quit_) 才能退出循环。

// 事件循环,该函数不能跨线程调用
// 只能在创建该对象的线程中调用
void EventLoop::loop()
{// 断言当前处于创建该对象的线程中
  assertInLoopThread();
    while (!quit_)
    {
        pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);

        eventHandling_ = true;
        for (ChannelList::iterator it = activeChannels_.begin();
                it != activeChannels_.end(); ++it)
        {
            currentActiveChannel_ = *it;
            currentActiveChannel_->handleEvent(pollReturnTime_);
        }
        currentActiveChannel_ = NULL;
        eventHandling_ = false;
       <span style="color:#ff0000;"> doPendingFunctors();</span>
    }
}

// 为了使IO线程在空闲时也能处理一些计算任务
// 在I/O线程中执行某个回调函数,该函数可以跨线程调用
void EventLoop::runInLoop(const Functor& cb)
{
  if (isInLoopThread())
  {
    // 如果是当前IO线程调用runInLoop,则同步调用cb
    cb();
  }
  else
  {
    // 如果是其它线程调用runInLoop,则异步地将cb添加到队列,让IO线程处理
    queueInLoop(cb);
  }
}

void EventLoop::queueInLoop(const Functor& cb)
{
  {
  MutexLockGuard lock(mutex_);
  pendingFunctors_.push_back(cb);
  }

  // 调用queueInLoop的线程不是当前IO线程则需要唤醒当前IO线程,才能及时执行doPendingFunctors();

  // 或者调用queueInLoop的线程是当前IO线程(比如在doPendingFunctors()中执行functors[i]() 时又调用了queueInLoop())
  // 并且此时正在调用pending functor,需要唤醒当前IO线程
  // 因为在此时doPendingFunctors() 过程中又添加了任务,故循环回去poll的时候需要被唤醒返回,进而继续执行doPendingFunctors()

  // 只有当前IO线程的事件回调中调用queueInLoop才不需要唤醒
 //  即在handleEvent()中调用queueInLoop 不需要唤醒,因为接下来马上就会执行doPendingFunctors();
  if (!isInLoopThread() || callingPendingFunctors_)
  {
    wakeup();
  }
}

// 该函数只会被当前IO线程调用
void EventLoop::doPendingFunctors()
{
  std::vector<Functor> functors;
  callingPendingFunctors_ = true;

  {
  MutexLockGuard lock(mutex_);
  functors.swap(pendingFunctors_);
  }

  for (size_t i = 0; i < functors.size(); ++i)
  {
    functors[i]();
  }
  callingPendingFunctors_ = false;
}
关于doPendingFunctors 的补充说明

1、不是简单地在临界区内依次调用Functor,而是把回调列表swap到functors中,这样一方面减小了临界区的长度(意味着不会阻塞其它线程的queueInLoop()),另一方面,也避免了死锁(因为Functor可能再次调用queueInLoop())
2、由于doPendingFunctors()调用的Functor可能再次调用queueInLoop(cb),这时,queueInLoop()就必须wakeup(),否则新增的cb可能就不能及时调用了
3、muduo没有反复执行doPendingFunctors()直到pendingFunctors_为空而是每次poll 返回就执行一次,这是有意的,否则IO线程可能陷入死循环,无法处理IO事件。

总结一下就是:
假设我们有这样的调用:loop->runInLoop(run),说明想让IO线程执行一定的计算任务,此时若是在当前的IO线程,就马上执行run();如果是其他线程调用的,那么就执行queueInLoop(run),将run异步添加到队列,当loop内处理完事件后,就执行doPendingFunctors(),也就执行到了run();最后想要结束线程的话,执行quit。


参考:
《linux多线程服务端编程》
http://blog.youkuaiyun.com/yusiguyuan/article/details/40593721?utm_source=tuicool&utm_medium=referral


<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 的高效事件驱动模型。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值