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

li27z發表於2016-10-01

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";
  }
}

相關文章