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