(P29)muduo_base库源码分析:进程(线程)wait/notify

1.进程(线程)wait/notify

  • 一个线程通知一个等待中的线程,方法有3个:

  • 方法1:pipe
    (1)fd[0]对应管道的读端,fd[1]对应管道的写端,fd[0]只能用于读,不能用于写,fd[1]只能用于写,不能用于读,这意味着管道是单向的
    (2)等待线程等待fd[0]的可读事件,通知线程只要往fd[1]写入一个数据,fd[0]就变的可读了,等待线程就获得通知,就被唤醒了;

  • 方法2:socketpair
    (1)也有一对fd,任意一个fd都是既可读也可写的,与管道的区别在于,它可以用于双向通信;

  • 方法3:eventfd
    (1)只有一个fd,等待线程关注fd的可读事件,通知线程只要往fd写入一个数据,等待线程就获得通知,就被唤醒了;
    (2)muduo的线程唤醒使用evenfd
    (3)eventfd 是一个比 pipe 更高效的线程间事件通知机制,一方面它比 pipe 少用一个 file descripor,节省了资源;另一方面,eventfd 的缓冲区管理也简单得多,全部“buffer” 只有定长8 bytes,不像 pipe 那样可能有不定长的真正 buffer。

  • 总结
    (1)上面的3个方法还可以用于进程间的等待通知,线程间的通知方法除了上述3种,还可以用条件变量。
    (2)条件变量与上述3方法区别在于,他们都是fd,可以使用I/O复用来管理(poll,epoll)

  • 类图
    一个EventLoop对象可以包含多个Channel对象,他们是聚合关系,EventLoop对象不负责Channel的生存期;
    Channel作为TcpConnection,Acceptor,Connector的成员,是组合的关系,Channel对象的生存期是由这些对象来控制的;
    eg:29\jmuduo\muduo\net\TimerQueue.h中的 Channel timerfdChannel_;
    在这里插入图片描述

  • eg:29\jmuduo\muduo\net\EventLoop.h
    29\jmuduo\muduo\net\EventLoop.cc

  • runInLoop()流程图
    若调用runInLoop()的线程是当前I/O线程,isInLoopThread()若是当前线程要执行回调任务,那么就直接同步的调用cb;
    若不是当前线程调用了runInLoop(),queueInLoop()那么把任务添加到线程的队列中,以便I/O线程能够执行该任务;
    在这里插入图片描述

  • EventLoop::runInLoop

// 在I/O线程中执行某个回调函数,该函数可以跨线程调用
void EventLoop::runInLoop(const Functor& cb)
{
  if (isInLoopThread())
  {
    // 如果是当前IO线程调用runInLoop,则同步调用回调函数(任务)cb
    cb();
  }
  else
  {
    // 如果是其它线程调用runInLoop,则异步地将cb添加到队列
    //以便让EventLoop所对应的I/O线程来执行该回调函数
    queueInLoop(cb);
  }
}

  • EventLoop::queueInLoop
//将任务cb添加到队列中
void EventLoop::queueInLoop(const Functor& cb)
{
  {
  MutexLockGuard lock(mutex_);
  pendingFunctors_.push_back(cb);//将任务添加到一个任务队列pendingFunctors中
  }
//其他线程往I/O线程中添加一个任务,那么就需要唤醒这个I/O线程,以便其能
//及时地执行该任务
  // 调用queueInLoop的线程不是当前IO线程需要唤醒
  // 或者调用queueInLoop的线程是当前IO线程,并且此时正在调用pending functor,需要唤醒, if (!isInLoopThread() || callingPendingFunctors_)
  // 只有当前IO线程的事件回调中调用queueInLoop才不需要唤醒
  if (!isInLoopThread() || callingPendingFunctors_)
  {
    wakeup();
  }
}

  • EventLoop::loop
 while (!quit_)
  {
    activeChannels_.clear();
    pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);//返回通道activeChannels_
    if (Logger::logLevel() <= Logger::TRACE)
    {
      printActiveChannels();
    }
    eventHandling_ = true;
    for (ChannelList::iterator it = activeChannels_.begin();
        it != activeChannels_.end(); ++it)
    {
      currentActiveChannel_ = *it;
      currentActiveChannel_->handleEvent(pollReturnTime_);//handleEvent处理这些事件
    }
    currentActiveChannel_ = NULL;
    eventHandling_ = false;
   //事件处理完毕后,调用
    doPendingFunctors();//向当前I/O线程添加回调任务,目的是让I/O
	//线程也能执行一些计算任务。因为I/O线程不是很繁忙的时候,会一直处于阻塞不工作的状态。
  }

  • 队列queueInLoop()流程图
    (1)若不是当前I/O线程((!isInLoopThread() == true时),则需要wakeup,A线程要把任务cb添加到B线程(他是I/O线程)的任务队列中,那么cb需要能够及时的被B执行,所以需要唤醒B,以便它能够执行。
    B是I/O线程,它处于loop中,poll()唤醒它,以便它能够执行到doPendingFunctors(),
    (2)若调用queueInLoop是当前线程((!isInLoopThread() == false时),并且此时正在调用pending functor,需要唤醒。这种情况只可能是doPendingFunctors()中又调用了queueInLoop(),即:当前IO线程处于doPendingFunctors(),在这个函数内部又调用了queueInLoop(),也需要唤醒当前的IO线程poll,否则doPendingFunctors()都执行了完毕之后,回来到poll,但是doPendingFunctors()这里面又调用了queueInLoop(),又添加了个任务,你没办法唤醒它,就没有办法及时的处理。
    (3)只有IO线程的事件回调中调用queueInLoop才不需要唤醒,在handleEvent中调用queueInLoop,将cb添加到队列中是不需要唤醒的,因为handleEvent处理完毕之后,会跑到doPendingFunctors(),所以说就不需要唤醒了。
    在这里插入图片描述

  • doPendingFunctors

// 当调用该函数时,就处于调用这些回调任务的状态中
void EventLoop::doPendingFunctors()
{
  //定义了一个空的向量
  std::vector<Functor> functors;
  callingPendingFunctors_ = true;

  {
    // 只保护这块内容的临界区
  MutexLockGuard lock(mutex_);
  functors.swap(pendingFunctors_);//交换完毕后,pendingFunctors_就变成了空
  }                               //pendingFunctors_的回调任务都放到了functors中
  /*
  下面的内容没有保护,这是为啥呢?
(1)不是简单地在临界区内依次调用Functor,而是把回调列表swap到functors中,这样一方面减小了临界区的长度(意味着不会阻塞其它线程的queueInLoop():
  又把回调任务添加进了pendingFunctors_,因为已经将pendingFunctors_交换到functors,下面的代码此时是不会执行的,所以下面的代码不需要取加锁)
  另一方面,也避免了死锁(因为Functor可能再次调用queueInLoop())
  lock//若其位置在callingPendingFunctors_ = true;之下
    lock(该lock来自queueInLoop())
  unlock//若其位置在 callingPendingFunctors_ = false;之上
  由于此时还没有unlock,中间的是没有办法获得lock锁,递归了就会处于死锁的状态

(2)由于doPendingFunctors()调用的Functor可能再次调用queueInLoop(cb),这时,queueInLoop()就必须wakeup(),否则新增的cb可能就不能及时调用了

(3)muduo没有在loop()函数中反复执行doPendingFunctors()直到pendingFunctors为空,这是有意的,否则IO线程可能陷入死循环,无法处理IO事件。
因为doPendingFunctors()调用了functors(),又调用了queueInLoop()将任务添加进来,那么loop()中的doPendingFunctors()可能永远无法执行到空,
所以没有必要将所有的任务执行完毕,只是将交换出来的任务执行完毕而已。
  */
  // 遍历functors列表,执行它
  for (size_t i = 0; i < functors.size(); ++i)
  {
    // functors回调任务函数
    functors[i]();
  }
  callingPendingFunctors_ = false;
}
  • eg:29\jmuduo\muduo\net\EventLoop.cc
    29\jmuduo\muduo\net\TimerQueue.cc

  • eg测试:29\jmuduo\tests\Reactor_test05.cc
    29\jmuduo\tests\CMakeLists.txt

  • 测试:
    在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

喜欢打篮球的普通人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值