muduo網絡庫學習筆記(11):有用的runInLoop()函數

runInLoop()函數的有用之處

“EventLoop有一個非常有用的功能:在它的IO線程內執行某個用戶任務回調,即EventLoop::runInLoop(const Functor& cb),其中Functor是boost::function<void()>。如果用戶在當前IO線程調用這個函數,回調會同步進行;如果用戶在其他線程調用runInLoop(),cb會被加入隊列,IO線程會被喚醒來調用這個Functor。”

即我們可以在線程間方便地進行任務調配,而且可以在不用鎖的情況下保證線程安全。

下面通過對代碼的分析來一探究竟。

源碼分析

(1)開門見山,我們先來看runInLoop()函數
流程圖:
這裏寫圖片描述

代碼片段1:EventLoop::runInLoop()
文件名:EventLoop.cc

// 在IO線程中執行某個回調函數,該函數可以跨線程調用
void EventLoop::runInLoop(const Functor& cb)
{
  if (isInLoopThread())
  {
    // 如果是當前IO線程調用runInLoop,則同步調用cb
    cb();
  }
  else
  {
    // 如果是其它線程調用runInLoop,則異步地將cb添加到隊列
    queueInLoop(cb);
  }
}

函數的邏輯很簡單:判斷是否處於當前IO線程,是則執行這個函數,如果不是則將函數加入隊列。

(2)queueInLoop()函數
流程圖:
這裏寫圖片描述

代碼片段2:EventLoop::queueInLoop()
文件名:EventLoop.cc

void EventLoop::queueInLoop(const Functor& cb)
{
  // 把任務加入到隊列可能同時被多個線程調用,需要加鎖
  {
  MutexLockGuard lock(mutex_);
  pendingFunctors_.push_back(cb);
  }

  // 將cb放入隊列後,我們還需要在必要的時候喚醒IO線程來處理
  // 必要的時候有兩種情況:
  // 1.如果調用queueInLoop()的不是IO線程,需要喚醒
  // 2.如果在IO線程調用queueInLoop(),且此時正在調用pending functor,需要喚醒
  // 即只有在IO線程的事件回調中調用queueInLoop()才無需喚醒
  if (!isInLoopThread() || callingPendingFunctors_)
  {
    wakeup();
  }
}

喚醒的時間點是怎麼選擇的呢?我們來回顧一下事件循環EventLoop::loop()中的一段代碼:

代碼片段3:EventLoop::loop()部分
文件名:EventLoop.cc

while (!quit_)
  {
    activeChannels_.clear();
    pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
    for (ChannelList::iterator it = activeChannels_.begin();
        it != activeChannels_.end(); ++it)
    {
      currentActiveChannel_ = *it;
      currentActiveChannel_->handleEvent(pollReturnTime_);
    }
    // 執行pending Functors_中的任務回調
    // 這種設計使得IO線程也能執行一些計算任務,避免了IO線程在不忙時長期阻塞在IO multiplexing調用中
    doPendingFunctors();
  }

這裏寫圖片描述
I.第一種情況易理解:調用queueInLoop的線程不是當前IO線程時,則需要喚醒當前IO線程,才能及時執行doPendingFunctors()。

II.第二種情況,調用queueInLoop()的線程是當前IO線程,比如在doPendingFunctors()中執行functors[i]() 時又調用了queueInLoop()。此時doPendingFunctors() 執行functors[i]() 過程中又添加了任務,故循環回去到poll的時候需要被喚醒返回,進而繼續執行doPendingFunctors() 。

只有在當前IO線程的事件回調中調用queueInLoop纔不需要喚醒,即在handleEvent()中調用queueInLoop ()不需要喚醒,因爲接下來馬上就會執行doPendingFunctors()。

(3)doPendingFunctors()函數
EventLoop::doPendingFunctors()不是簡單地在臨界區依次調用Functor,而是把回調列表swap()到局部變量functors中,這樣做,一方面減小了臨界區的長度(不會阻塞其他線程調用queueInLoop()),另一方面避免了死鎖(因爲Functor可能再調用queueInLoop())。

代碼片段4:EventLoop::doPendingFunctors()
文件名:EventLoop.cc

void EventLoop::doPendingFunctors()
{
  std::vector<Functor> functors;
  callingPendingFunctors_ = true;

  // 把回調列表swap()到局部變量functors中
  {
  MutexLockGuard lock(mutex_);
  functors.swap(pendingFunctors_);
  }

  // 依次執行回調列表中的函數
  for (size_t i = 0; i < functors.size(); ++i)
  {
    functors[i]();
  }
  callingPendingFunctors_ = false;
}

muduo這裏沒有反覆執行doPendingFunctors()直到pendingFunctors_爲空,反覆執行可能會使IO線程陷入死循環,無法處理IO事件。

(4)我們回頭再來看一下–怎樣實現喚醒
傳統的進程/線程間喚醒辦法是用pipe或者socketpair,IO線程始終監視管道上的可讀事件,在需要喚醒的時候,其他線程向管道中寫一個字節,這樣IO線程就從IO multiplexing阻塞調用中返回。pipe和socketpair都需要一對文件描述符,且pipe只能單向通信,socketpair可以雙向通信。

下面介紹一下muduo所採用的一種高效的進程/線程間事件通知機制–eventfd。

// 頭文件
#include <sys/eventfd.h> 

// 爲事件通知創建文件描述符
// 參數initval表示初始化計數器值
// 參數flags可取EFD_NONBLOCK、EFD_CLOEXEC、EFD_SEMAPHORE 
int eventfd(unsigned int initval, int flags);

它的高效體現在:一方面它比 pipe 少用一個 fd,節省了資源;另一方面,eventfd 的緩衝區管理也簡單得多,全部buffer只有定長8 bytes,不像 pipe 那樣可能有不定長的真正 buffer。

代碼片段5:EventLoop::wakeup()
文件名:EventLoop.cc

void EventLoop::wakeup()
{
  uint64_t one = 1;
  // 向wakupFd_中寫入8字節從而喚醒,wakeupFd_即eventfd()所創建的文件描述符
  ssize_t n = ::write(wakeupFd_, &one, sizeof one);
  if (n != sizeof one)
  {
    LOG_ERROR << "EventLoop::wakeup() writes " << n << " bytes instead of 8";
  }
}
發佈了44 篇原創文章 · 獲贊 34 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章