muduo-net 源码分析(四)—— 跨线程调用

先序

再上一章节中我们修改了 TimerQueue 文件中 addTimer()cancel() 的实现函数;实际上这两个函数都是可以跨线程使用的,并且在大部分情况下都应该跨线程使用;实现跨线程调用的原因得益于 runInLoop(),这也是本节讨论的重点;而这部分就是 EvenLoop 剩下没有讨论完的部分,所以我们这节也是要完善 EvenLoop

//可跨线程使用
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));
  
  /*不能跨线程的代码
  addTimerInLoop(timer);
  */
  return TimerId(timer, timer->sequence());
}

//可跨线程使用
void TimerQueue::cancel(TimerId timerId)
{
  //可以跨线程的代码
  loop_->runInLoop(
      std::bind(&TimerQueue::cancelInLoop, this, timerId));
  //不能跨线程的代码
  //cancelInLoop(timerId);
}

EvenLoop 源码分析【剩余部分】

主要的变量

bool callingPendingFunctors_;						//调用 pendingFunc 中
std::unique_ptr<TimerQueue> timerQueue_;			//定时器,这里使用 unique_ptr,说明定时器的声明周期受 EvenLoop 对象控制
int wakeupFd_;										//唤醒文件描述符
std::unique_ptr<Channel> wakeupChannel_;			//监听唤醒文件描述符的通道
mutable MutexLock mutex_;
std::vector<Functor> pendingFunctors_ GUARDED_BY(mutex_);		//pendingFunc 数组

对于 evenLoop 来说,他只控制一个 channel 的生命周期,而这个 channel 就是 wakeupChannel

主要的函数

//重点
void runInLoop(Functor cb);
void queueInLoop(Functor cb);

TimerId runAt(Timestamp time, TimerCallback cb);
TimerId runAfter(double delay, TimerCallback cb);
TimerId runEvery(double interval, TimerCallback cb);
void cancel(TimerId timerId);

void wakeup();
void handleRead();

void doPendingFunctors();

具体实现

对于新增部分我会标记出来,重点看新增部分

EventLoop::EventLoop()
  : looping_(false),
    quit_(false),
    eventHandling_(false),
    callingPendingFunctors_(false),
    iteration_(0),
    threadId_(CurrentThread::tid()),
    //指向默认的派生类对象,需要为派生类传入 EvenPoll 对象
    poller_(Poller::newDefaultPoller(this)),
    ----------------------------------------------
    //这里创建了 TimerQueue 对象,并监听了它的可读事件
    timerQueue_(new TimerQueue(this)),
    //wakeupFd_ 是用于线程之间通知的文件描述符,这是实现跨线程的核心
    wakeupFd_(createEventfd()),
    //关注 wakeupFd
    wakeupChannel_(new Channel(this, wakeupFd_)),
    -----------------------------------------------
    currentActiveChannel_(NULL)
{
  LOG_DEBUG << "EventLoop created " << this << " in thread " << threadId_;
  //判断当前线程是否已经存在 EvenLoop
  if (t_loopInThisThread)
  {
    LOG_FATAL << "Another EventLoop " << t_loopInThisThread
              << " exists in this thread " << threadId_;
  }
  else
  {
    t_loopInThisThread = this;
  }
  -----------------------------------------------
  //wakeupfd 的读回调函数设置
  wakeupChannel_->setReadCallback(
      std::bind(&EventLoop::handleRead, this));
  //监听读事件
  wakeupChannel_->enableReading();
  -----------------------------------------------
}

int createEventfd()
{
  //使用 evenfd 来创建一个文件描述符,用于线程之间通知
  int evtfd = ::eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
  if (evtfd < 0)
  {
    LOG_SYSERR << "Failed in eventfd";
    abort();
  }
  return evtfd;
}

构造函数除了之前描述的功能外,还创建了一个定时器以及创建了一个用于线程之间通信的文件描述符

EventLoop::~EventLoop()
{
  LOG_DEBUG << "EventLoop " << this << " of thread " << threadId_
            << " destructs in thread " << CurrentThread::tid();
  -------------------------------------------------------------- 
  //注意定时器使用的是 unique_ptr 会自动释放
  //wakeupfd 和管道需要我们手动处理
  wakeupChannel_->disableAll();
  wakeupChannel_->remove();
  ::close(wakeupFd_);
  -------------------------------------------------------------
  t_loopInThisThread = NULL;
}

以下函数都是新增的;

//这个函数是跨线程的核心函数,外部接口
void EventLoop::runInLoop(Functor cb)
{
  //如果是在当前线程中调用,则同步调用 cb,正常处理情况
  if (isInLoopThread())
  {
    cb();
  }
  //如果是在其他线程调用,则异步地将 cb 添加到队列中
  else
  {
    queueInLoop(std::move(cb));
  }
}

//因为是跨线程调用,所以要保证线程安全
void EventLoop::queueInLoop(Functor cb)
{
  {
  MutexLockGuard lock(mutex_);
  pendingFunctors_.push_back(std::move(cb));
  }
  
  //如果不在 IO 线程中就要唤醒 IO 线程【防止 poll() 还在等待事件发生】
  //为什么需要第二种情况?【可以细看 TcpServer::removeConnection 函数】
  if (!isInLoopThread() || callingPendingFunctors_)
  {
    //因为 poll 监听的事件中多了一个 wakeupfd,就是防止 poll 一直阻塞等待
    //其他线程写入内容激发其可读事件
    wakeup();
  }
}

void EventLoop::wakeup()
{
  //8个字节的缓冲区
  uint64_t one = 1;
  //wakeupfd 对应的文件描述符的可读事件被激活
  ssize_t n = sockets::write(wakeupFd_, &one, sizeof one);
  if (n != sizeof one)
  {
    LOG_ERROR << "EventLoop::wakeup() writes " << n << " bytes instead of 8";
  }
}
  • runInLoop() 如果是在当前 IO 线程中被调用则和正常不跨线程的函数一样,直接调用;
  • 如果不在当前 IO 线程中被调用则会将该函数加入到队列中,并唤醒 wakeupfd
  • loop 则会处理 wakeupfd 的可读事件,wakeupfd 的作用很明显就是其他线程用来通知当前线程,所以我们只需要将其内核缓冲区的内容读走就可以了;
//wakeupfd 的可读事件
void EventLoop::handleRead()
{
  uint64_t one = 1;
  ssize_t n = sockets::read(wakeupFd_, &one, sizeof one);
  if (n != sizeof one)
  {
    LOG_ERROR << "EventLoop::handleRead() reads " << n << " bytes instead of 8";
  }
}

void EventLoop::loop()
{
  assert(!looping_);
  assertInLoopThread();
  looping_ = true;
  quit_ = false;  // FIXME: what if someone calls quit() before loop() ?
  LOG_TRACE << "EventLoop " << this << " start looping";

  while (!quit_)
  {
    activeChannels_.clear();
    pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
    ++iteration_;
    if (Logger::logLevel() <= Logger::TRACE)
    {
      printActiveChannels();
    }
    // TODO sort channel by priority
    //处理事件
    eventHandling_ = true;

    //遍历所以事件,调用 handleEvent 的处理函数
    for (Channel* channel : activeChannels_)
    {
      currentActiveChannel_ = channel;
      currentActiveChannel_->handleEvent(pollReturnTime_);
    }
    //事件处理结束,当前没有活跃事件
    currentActiveChannel_ = NULL;
    eventHandling_ = false;
    ---------------------------------------------
    //doPendingFunctors 用来处理其他线程调用的 func
    doPendingFunctors();
    ---------------------------------------------
  }

  LOG_TRACE << "EventLoop " << this << " stop looping";
  looping_ = false;
}


------------------------------------------------------------------
void EventLoop::doPendingFunctors()
{
  std::vector<Functor> functors;

  //处理添加的 func 中
  callingPendingFunctors_ = true;
  
  //加锁的原因是,其他线程会让里面添加内容
  //交换的原因是为什么?
  //如果我们一直加锁直到函数处理结束,那么其他调用 runInLoop() 的线程就无法将函数假如队列中,从而阻塞线程很长时间
  {
  MutexLockGuard lock(mutex_);
  functors.swap(pendingFunctors_);
  }
 
  //此时 pendingFunctors_ 的任务都放到 functors 中
  //执行其他线程添加的 func
  for (const Functor& functor : functors)
  {
    functor();
  }
  callingPendingFunctors_ = false;
}
--------------------------------------------------------------------
  • EvenLoop 会在处理完可读事件后检查列表中是否有其他线程传入的函数,并在 IO 线程中执行这行函数;
//quit() 是可以跨线程使用的,唤醒 poll() 之后处理完所有事件后停止循环
void EventLoop::quit()
{
  quit_ = true;

------------------------------
  if (!isInLoopThread())
  {
    wakeup();
  }
------------------------------
}

可以在其他线程中关闭 EvenLooploop 函数,且大多数情况下应该在其他线程中关闭;
以下是 EvenLoop 调用定时器的方法

//addTimer 实际上就是使用 addTimerInLoop
//如果在当前 IO 线程中使用则直接运行,即激活一次性定时器
//如果是在其他线程中使用,则将 addTiemrInLoop 函数放入到 pendingFunctors_ 中,等待被执行
//实现的功能是在 time 时到期触发 cb
TimerId EventLoop::runAt(Timestamp time, TimerCallback cb)
{
  return timerQueue_->addTimer(std::move(cb), time, 0.0);
}

//在距离现在 delay s 后到期,触发 cb
TimerId EventLoop::runAfter(double delay, TimerCallback cb)
{
  Timestamp time(addTime(Timestamp::now(), delay));
  return runAt(time, std::move(cb));
}

//每隔 interval s 后到期,触发 cb【周期性】
TimerId EventLoop::runEvery(double interval, TimerCallback cb)
{
  Timestamp time(addTime(Timestamp::now(), interval));
  return timerQueue_->addTimer(std::move(cb), time, interval);
}

//取消 timerId 对应的定时器
void EventLoop::cancel(TimerId timerId)
{
  return timerQueue_->cancel(timerId);
}

test

#include "muduo/net/EventLoop.h"
#include "muduo/base/Thread.h"

#include <assert.h>
#include <stdio.h>
#include <unistd.h>

using namespace muduo;
using namespace muduo::net;

EventLoop* g_loop;

void callback()
{
  printf("callback(): pid = %d, tid = %d\n", getpid(), CurrentThread::tid());
  //这个线程已经创建过 eventloop 了,报错
  EventLoop anotherLoop;
}

void threadFunc()
{
  printf("threadFunc(): pid = %d, tid = %d\n", getpid(), CurrentThread::tid());

  assert(EventLoop::getEventLoopOfCurrentThread() == NULL);
  EventLoop loop;
  assert(EventLoop::getEventLoopOfCurrentThread() == &loop);
  //1s 后会调用 callback,此时线程中已经有 loop 了,调用 callback 会报错
  loop.runAfter(1.0, callback);
 
  loop.loop();
}

int main()
{
  printf("main(): pid = %d, tid = %d\n", getpid(), CurrentThread::tid());

  assert(EventLoop::getEventLoopOfCurrentThread() == NULL);
  EventLoop loop;
  assert(EventLoop::getEventLoopOfCurrentThread() == &loop);

  //线程会调用 threadFunc 函数
  Thread thread(threadFunc);
  thread.start();

  loop.loop();
}

在这里插入图片描述

#include "muduo/net/EventLoop.h"
#include "muduo/net/EventLoopThread.h"
#include "muduo/base/Thread.h"

#include <stdio.h>
#include <unistd.h>

using namespace muduo;
using namespace muduo::net;

int cnt = 0;
EventLoop* g_loop;

void printTid()
{
  printf("pid = %d, tid = %d\n", getpid(), CurrentThread::tid());
  printf("now %s\n", Timestamp::now().toString().c_str());
}

void print(const char* msg)
{
  printf("msg %s %s\n", Timestamp::now().toString().c_str(), msg);
  if (++cnt == 20)
  {
    g_loop->quit();
  }
}

void cancel(TimerId timer)
{
  g_loop->cancel(timer);
  printf("cancelled at %s\n", Timestamp::now().toString().c_str());
}

int main()
{
  printTid();
  sleep(1);
  {
    EventLoop loop;
    //g_loop 是全局变量,全局变量会被多线程共享,便于其他线程访问
    g_loop = &loop;

    print("main");
    //1s 后调用 print 打印一次 once1
    loop.runAfter(1, std::bind(print, "once1"));

    //1.5s 后调用 print 打印一次 once1.5
    loop.runAfter(1.5, std::bind(print, "once1.5"));

    //2.5s 后调用 print 打印一次 once2.5
    loop.runAfter(2.5, std::bind(print, "once2.5"));


    //3.5 s 后调用 print 打印一次 once3.5
    loop.runAfter(3.5, std::bind(print, "once3.5"));

    //4.5s 后调用 print 打印一次 once4.5
    TimerId t45 = loop.runAfter(4.5, std::bind(print, "once4.5"));
    
    //4.2 s 调用 looper 的 cancel 函数取消 4.5 s 的打印
    loop.runAfter(4.2, std::bind(cancel, t45));

    //4.8 s 取消 t45 定时器,但因为定时器已经不在了,所以没用
    loop.runAfter(4.8, std::bind(cancel, t45));
    
    //每隔 2s 打印一次 every2
    loop.runEvery(2, std::bind(print, "every2"));
    
    //每隔 3s 打印一次 every3
    TimerId t3 = loop.runEvery(3, std::bind(print, "every3"));
    
    //在 9.001 秒时调用 cannel 取消每次 3s 的定时器
    loop.runAfter(9.001, std::bind(cancel, t3));

    loop.loop();
    print("main loop exits");
  }
}

在这里插入图片描述
在这里插入图片描述
注:好像这里有 bug9.001s 后应该取消每隔 3s 的定时;那么应该先调用每隔 3s 的函数,然后在 9.001s 后将每隔 3s 的定时任务删除;但实际的情况有两种:一种先调用取消函数,然后调用每隔 3s 的周期函数;第二种是直接取消,不会调用每隔 3s 的周期函数


合理的推测是:

  1. 调用 handleread 时,会重新统计当前时间,而不是使用触发定时器的时间;所以此时 9s9.001s 可能都到期了;
  2. 为什么每隔 3s 的定时任务会在取消后执行呢?因为程序是顺序执行的,并且在 eventLoop 线程中执行,所以并不是将添加任务放入到队列中,而是直接执行,执行完后再执行下一个程序;而每一个 runAfterrunEvery 都是在当前时间上加上间隔,所以要看每隔 3s 和取消这两个代码之间的运行时间;从结果上来看运行时间超过了 0.001s
  3. 综合上面的推测:在 9s 左右触发了定时器,根据调用 handleread() 的时间,如果取消 3s 周期和 3s 周期都触发,则先调用取消 3s 周期的函数,此时找不到 3s 周期的定时器,只能添加到 cancelingTimers_ 中;后调用 3s 周期函数,但无法重置; 如果只有取消 3s 周期触发,则直接删除;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值
>