chromium中的消息循環

這篇文章會更加深入地講解chromium的線程,通常爲了保證ui的響應速度,防止io阻塞和複雜計算卡死ui,會把io任務和複雜計算的任務放到其它線程中運行。由於反覆創建線程會增加系統負擔,在設計的時候通常不會把任務交給線程,在線程運行完成後直接退出線程。chromium中每一個線程都會有一個消息循環MessageLoop,接收來自其它線程的任務,在處理完成後接着處理下一個任務,當沒有任務時,掛起線程,等待任務。MessageLoop的類圖如下:

根據MessageLoop的類型,在MessageLoop中會實例化不同的MessagePump,在windows環境下,有三種MessagePump,分別是MessagePumpDefault、MessagePumpForUI、MessagePumpForIO。其中MessagePumpDefault只處理Task,MessagePumpForUI會處理windows消息和Task,MessagePumpForIO會處理IO事件和Task。由於在沒有計算任務或者等待windows消息和io事件時,線程會掛起,爲了使計算任務不會在線程掛起時會“餓死”,MessagePump中還定義了ScheduleWork函數,在有計算任務的時候進行調用,使得計算任務能及時調度。

MessagePump的Run函數是執行消息循環的過程,不同的MessagePump的實現如下

void MessagePumpDefault::Run(Delegate* delegate) {
  for (;;) {
    bool did_work = delegate->DoWork();
    if (!keep_running_)
      break;
    did_work |= delegate->DoDelayedWork(&delayed_work_time_);
    if (!keep_running_)
      break;
    if (did_work)
      continue;
    did_work = delegate->DoIdleWork();
    if (!keep_running_)
      break;
    if (did_work)
      continue;
    ThreadRestrictions::ScopedAllowWait allow_wait;
    if (delayed_work_time_.is_null()) {
      event_.Wait();
    } else {
      TimeDelta delay = delayed_work_time_ - TimeTicks::Now();
      if (delay > TimeDelta()) {
        event_.TimedWait(delay);
      } else {
        delayed_work_time_ = TimeTicks();
      }
    }
  }
  keep_running_ = true;
}
void MessagePumpForUI::DoRunLoop() {
  for (;;) {
    bool more_work_is_plausible = ProcessNextWindowsMessage();
    if (state_->should_quit)
      break;
    more_work_is_plausible |= state_->delegate->DoWork();
    if (state_->should_quit)
      break;
    more_work_is_plausible |=
        state_->delegate->DoDelayedWork(&delayed_work_time_);
    if (more_work_is_plausible && delayed_work_time_.is_null())
      KillTimer(message_hwnd_, reinterpret_cast<UINT_PTR>(this));
    if (state_->should_quit)
      break;
    if (more_work_is_plausible)
      continue;
    more_work_is_plausible = state_->delegate->DoIdleWork();
    if (state_->should_quit)
      break;
    if (more_work_is_plausible)
      continue;
    WaitForWork();  // Wait (sleep) until we have work to do again.
  }
}
void MessagePumpForIO::DoRunLoop() {
  for (;;) {
    bool more_work_is_plausible = state_->delegate->DoWork();
    if (state_->should_quit)
      break;
    more_work_is_plausible |= WaitForIOCompletion(0, NULL);
    if (state_->should_quit)
      break;
    more_work_is_plausible |=
        state_->delegate->DoDelayedWork(&delayed_work_time_);
    if (state_->should_quit)
      break;
    if (more_work_is_plausible)
      continue;
    more_work_is_plausible = state_->delegate->DoIdleWork();
    if (state_->should_quit)
      break;
    if (more_work_is_plausible)
      continue;
    WaitForWork();  // Wait (sleep) until we have work to do again.
  }
}

主循環中使用到的delegate就是MessageLoop,因爲它實現了MessagePump::Delegate接口。主循環中不同的地方只是等待任務的方式有些區別,其它的部分就是調用MessageLoop的DoWork()、DoDelayedWork()、DoIdleWork()函數。由於不同的MessagePump處理的任務有些區別,它們的等待機制實現也有所不同,喚醒線程的方式也有所區別。

(1)MessagePumpDefault等待消息使用信號量event_,如果沒有延時任務,則進入無限期等待,如果有延時任務,等待相應的延時後喚醒。它喚醒線程的方式也很簡單,只需要對信號量進行設置。

 

//等待消息
if (delayed_work_time_.is_null()) {
  event_.Wait();
} else {
  TimeDelta delay = delayed_work_time_ - TimeTicks::Now();
  if (delay > TimeDelta()) {
    event_.TimedWait(delay);
  } else {
    delayed_work_time_ = TimeTicks();
  }
}
//喚醒線程
void MessagePumpDefault::ScheduleWork() {
  event_.Signal();
}
(2)MessagePumpForUI調用MsgWaitForMultipleObjectsEx等待系統的信號量,這個函數是阻塞的。當檢查到有信號量時,返回。但有一種情況比較特別,就是存在父子關係的窗口關係,當一個線程中調用MsgWaitForMultipleObjectsEx檢查到有消息後,另一個線程可能已經調用PeekMessage把消息取走,這時候PeekMessage會取不到消息,導致WaitForWork返回,程序運行一個空循環,像鼠標事情這種比較頻繁的信號,多次運行空循環會增加系統cpu負擔,這時候當檢查到隊列中有鼠標消息但是PeekMessage又取不到,就會進入等待(這部份代碼的意圖我沒有完全吃透,到以後對windows編程理解更深後再回來看看)。

MessagePumpForUI通過往窗口消息隊列中加入一條類型爲kMsgHaveWork的消息來實現,have_work這個標記是用來保證窗口消息隊列中只能有一條kMsgHaveWork消息,InterlockedExchange這個函數是把變量值設置成第二個參數值,並返回原始值,它是一個原子操作,可以理解成是加鎖的,線程安全。可以發現當消息隊列中如果有kMsgHaveWork消息,也就是have_work的值爲1時,ScheduleWork會直接返回,否則會調用PostMessage往消息隊列中加入一條MsgHaveWork消息,WaitForWork等待到消息時,會喚醒線程。

void MessagePumpForUI::WaitForWork() {
  int delay = GetCurrentDelay();
  if (delay < 0)  // Negative value means no timers waiting.
    delay = INFINITE;
  DWORD result;
  result = MsgWaitForMultipleObjectsEx(0, NULL, delay, QS_ALLINPUT,
                                       MWMO_INPUTAVAILABLE);
  if (WAIT_OBJECT_0 == result) {
    MSG msg = {0};
    DWORD queue_status = GetQueueStatus(QS_MOUSE);
    if (HIWORD(queue_status) & QS_MOUSE &&
        !PeekMessage(&msg, NULL, WM_MOUSEFIRST, WM_MOUSELAST, PM_NOREMOVE)) {
      WaitMessage();
    }
    return;
  }
}
void MessagePumpForUI::ScheduleWork() {
  if (InterlockedExchange(&have_work_, 1))
    return;  // Someone else continued the pumping.
  BOOL ret = PostMessage(message_hwnd_, kMsgHaveWork,
                         reinterpret_cast<WPARAM>(this), 0);
  if (ret)
    return;  // There was room in the Window Message queue.
  InterlockedExchange(&have_work_, 0);  // Clarify that we didn't really insert.
}

(3)MessagePumpForIO使用的是windows完成端口模型,它的WaitForWork 邏輯也比較複雜,WaitForIOCompletion中的filter參數在代碼邏輯裏都是NULL,所以completed_io_通常都是空。GetIOItem中GetQueuedCompletionStatus檢查隊列中是否有I/O處理完成的結果。在完成端口模型中,當一個設備的異步I/O請求完成之後,系統會檢查該設備是否關聯了一個完成端口,如果是,系統就向該完成端口的I/O完成隊列中加入完成的I/O請求列。GetQueuedCompletionStatus使調用線程掛起,直到指定的端口的I/O完成隊列中出現了一項或直到超時。IOItem會保存傳輸的字節數,句柄和Overlapped結構地址,因爲句柄handler是一個指針,是4字節對齊,所以handler後兩位一定是0,在實現時會把handler和has_valid_io_context 的合併成一個key,在關聯完成端口時做爲參數輸入。GetIOItem返回後,會調用ProcessInternalIOItem,判斷是否是ScheduleWork中僞造的IO完成事件,如果不是調用item.handler->OnIOCompleted接口,處理IO完成後的數據。

MessagePumpForIO的ScheduleWork的實現和MessagePumpForUI很相似,當有任務需要調度時,會僞造一個IO完成事件,放入到完成隊列中,使GetQueuedCompletionStatus返回,喚醒線程。它同樣有一個標記位have_work_保證完成隊列中最多有一個僞造的IO完成事件。

void MessagePumpForIO::WaitForWork() {
  int timeout = GetCurrentDelay();
  if (timeout < 0)  // Negative value means no timers waiting.
    timeout = INFINITE;
  WaitForIOCompletion(timeout, NULL);
}

bool MessagePumpForIO::WaitForIOCompletion(DWORD timeout, IOHandler* filter) {
  IOItem item;
  if (completed_io_.empty() || !MatchCompletedIOItem(filter, &item)) {
    if (!GetIOItem(timeout, &item))
      return false;
    if (ProcessInternalIOItem(item))
      return true;
  }
  if (!item.has_valid_io_context || item.context->handler) {
    if (filter && item.handler != filter) {
      completed_io_.push_back(item);
    } else {
      DCHECK(!item.has_valid_io_context ||
             (item.context->handler == item.handler));
      WillProcessIOEvent();
      item.handler->OnIOCompleted(item.context, item.bytes_transfered,
                                  item.error);
      DidProcessIOEvent();
    }
  } else {
    delete item.context;
  }
  return true;
}
bool MessagePumpForIO::GetIOItem(DWORD timeout, IOItem* item) {
  memset(item, 0, sizeof(*item));
  ULONG_PTR key = NULL;
  OVERLAPPED* overlapped = NULL;
  if (!GetQueuedCompletionStatus(port_.Get(), &item->bytes_transfered, &key,
                                 &overlapped, timeout)) {
    if (!overlapped)
      return false;  // Nothing in the queue.
    item->error = GetLastError();
    item->bytes_transfered = 0;
  }
  item->handler = KeyToHandler(key, &item->has_valid_io_context);
  item->context = reinterpret_cast<IOContext*>(overlapped);
  return true;
}

bool MessagePumpForIO::ProcessInternalIOItem(const IOItem& item) {
  if (this == reinterpret_cast<MessagePumpForIO*>(item.context) &&
      this == reinterpret_cast<MessagePumpForIO*>(item.handler)) {
    InterlockedExchange(&have_work_, 0);
    return true;
  }
  return false;
}
// Returns a completion item that was previously received.
bool MessagePumpForIO::MatchCompletedIOItem(IOHandler* filter, IOItem* item) {
  DCHECK(!completed_io_.empty());
  for (std::list<IOItem>::iterator it = completed_io_.begin();
       it != completed_io_.end(); ++it) {
    if (!filter || it->handler == filter) {
      *item = *it;
      completed_io_.erase(it);
      return true;
    }
  }
  return false;
}
// static
ULONG_PTR MessagePumpForIO::HandlerToKey(IOHandler* handler,
                                         bool has_valid_io_context) {
  ULONG_PTR key = reinterpret_cast<ULONG_PTR>(handler);
  if (!has_valid_io_context)
    key = key | 1;
  return key;
}

// static
MessagePumpForIO::IOHandler* MessagePumpForIO::KeyToHandler(
    ULONG_PTR key,
    bool* has_valid_io_context) {
  *has_valid_io_context = ((key & 1) == 0);
  return reinterpret_cast<IOHandler*>(key & ~static_cast<ULONG_PTR>(1));
}

void MessagePumpForIO::ScheduleWork() {
  if (InterlockedExchange(&have_work_, 1))
    return;  // Someone else continued the pumping.
  BOOL ret = PostQueuedCompletionStatus(port_, 0,
                                        reinterpret_cast<ULONG_PTR>(this),
                                        reinterpret_cast<OVERLAPPED*>(this));
  if (ret)
    return;  // Post worked perfectly.
  InterlockedExchange(&have_work_, 0);  // Clarify that we didn't succeed.
  UMA_HISTOGRAM_ENUMERATION("Chrome.MessageLoopProblem", COMPLETION_POST_ERROR,
                            MESSAGE_LOOP_PROBLEM_MAX);
}
在描述完消息循環怎麼接收消息後,接下來需要說的就是處理消息的過程。首先最基礎的處理就是task的處理,它是在MessageLoop中完成的,MessageLoop實現了MessagePump::Delegate接口,它有三個主要的函數DoWork()、DoDelayedWork()、DoIdleWork()。在 DoWork的化碼中,ReloadWorkQueue的作用是把work_queue_與incoming_task_queue_交換,因爲在PostTask的函數中,新加的任務都先保存在incoming_task_queue_,在DoWork中才對任務置換到work_queue_中進行處理。對於延時任務,會把任務放到delayed_work_queue_中,delayed_work_queue_是用優先隊列實現的,它會把最先到期的時間通過ScheduleDelayedWork函數告訴MessagePump,使任務能及時喚醒。DeferOrRunPendingTask代碼中,run_loop_->run_depth_ == 1通常是true,因爲沒有發現存在嵌套的循環,所以這個函數可以理解成直接運行任務RunTask(pending_task)。DoDelayedWork就是從delayed_work_queue_中取出延時任務,由於它是一個優先隊列,隊列的top()就是最先到期的延時任務,如果它的延時時間沒有到就直接返回,如果時間到了就從隊列中取出top(),運行延時任務。在MessageLoop中,並沒有在棧上嵌套調用RunLoop::Run方式(因爲RunHandler並沒有RunLoop的實例),所以deferred_non_nestable_work_queue_應該是不會填充的,DoIdleWork中的ProcessNextDelayedNonNestableTask通常是返回false,但是並不是說DoIdleWork這個函數一定用也沒有,如果MessageLoop使用的是RunUntilIdle接口運行的,會把quit_when_idle_received_置成true,表示線程空閒的時候就退出了,那麼當運行到DoIdleWork時,調用 MessagePump::quit接口退出函數。

bool MessageLoop::DoWork() {
  if (!nestable_tasks_allowed_) {
    return false;
  }
  for (;;) {
    ReloadWorkQueue();
    if (work_queue_.empty())
      break;
    do {
      PendingTask pending_task = work_queue_.front();
      work_queue_.pop();
      if (!pending_task.delayed_run_time.is_null()) {
        AddToDelayedWorkQueue(pending_task);
        // If we changed the topmost task, then it is time to reschedule.
        if (delayed_work_queue_.top().task.Equals(pending_task.task))
          pump_->ScheduleDelayedWork(pending_task.delayed_run_time);
        } else {
          if (DeferOrRunPendingTask(pending_task))
            return true;
        }
    } while (!work_queue_.empty());
  }
  return false;
}
bool MessageLoop::DoDelayedWork(TimeTicks* next_delayed_work_time) {
  if (!nestable_tasks_allowed_ || delayed_work_queue_.empty()) {
    recent_time_ = *next_delayed_work_time = TimeTicks();
    return false;
  }
  TimeTicks next_run_time = delayed_work_queue_.top().delayed_run_time;
  if (next_run_time > recent_time_) {
    recent_time_ = TimeTicks::Now();  // Get a better view of Now();
    if (next_run_time > recent_time_) {
      *next_delayed_work_time = next_run_time;
      return false;
    }
  }
  PendingTask pending_task = delayed_work_queue_.top();
  delayed_work_queue_.pop();
  if (!delayed_work_queue_.empty())
    *next_delayed_work_time = delayed_work_queue_.top().delayed_run_time;
  return DeferOrRunPendingTask(pending_task);
}
bool MessageLoop::DoIdleWork() {
  if (ProcessNextDelayedNonNestableTask())
    return true;
  if (run_loop_->quit_when_idle_received_)
    pump_->Quit();
  return false;
}
bool MessageLoop::DeferOrRunPendingTask(const PendingTask& pending_task) {
  if (pending_task.nestable || run_loop_->run_depth_ == 1) {
    RunTask(pending_task);
    return true;
  }
  deferred_non_nestable_work_queue_.push(pending_task);
  return false;
}
bool MessageLoop::ProcessNextDelayedNonNestableTask() {
  if (run_loop_->run_depth_ != 1)
    return false;
  if (deferred_non_nestable_work_queue_.empty())
    return false;
  PendingTask pending_task = deferred_non_nestable_work_queue_.front();
  deferred_non_nestable_work_queue_.pop();
  RunTask(pending_task);
  return true;
}

MessagePumpForUI主要處理的消息是窗口消息,在ProcessNextWindowsMessage中,如果通過PeekMessage獲取到消息,則進入處理消息的過程ProcessMessageHelper。如果消息是退出消息,調用PostQuitMessage處理退出事件,這個比較好理解。對於kMsgHaveWork,在上文中我們提到這是MessagePumpForUI自己增加的消息,這個消息是不會通過窗口函數WndProcThunk處理的,這時會嘗試着重新取出一條消息進行處理,如果取到消息,就再創建一條kMsgHaveWork放到消息隊列中,表示後續可能還有任務,防止WaitForWork阻塞線程(我原本以爲這裏代碼的意思是處理一條窗口消息再處理一個task,可是後來我發現kMsgHaveWork消息不會被Dispatch到WndProcThunk進行處理,也就是WndProcThunk函數中處理kMsgHaveWork的邏輯是無效的,kMsgHaveWork在這裏的作用只是爲了防止WaitForWork阻塞線程,這個是我個人的看法,如果看官有自己的想法,歡迎留言)。對於其它的消息,則可能會對消息進行分發,或者是調用DispatchMessage到窗口函數WndProcThunk進行處理。
bool MessagePumpForUI::ProcessNextWindowsMessage() {
  bool sent_messages_in_queue = false;
  DWORD queue_status = GetQueueStatus(QS_SENDMESSAGE);
  if (HIWORD(queue_status) & QS_SENDMESSAGE)
    sent_messages_in_queue = true;
  MSG msg;
  if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) != FALSE)
    return ProcessMessageHelper(msg);
  return sent_messages_in_queue;
}

bool MessagePumpForUI::ProcessMessageHelper(const MSG& msg) {
  TRACE_EVENT1("base", "MessagePumpForUI::ProcessMessageHelper",
               "message", msg.message);
  if (WM_QUIT == msg.message) {
    state_->should_quit = true;
    PostQuitMessage(static_cast<int>(msg.wParam));
    return false;
  }
  if (msg.message == kMsgHaveWork && msg.hwnd == message_hwnd_)
    return ProcessPumpReplacementMessage();
  if (CallMsgFilter(const_cast<MSG*>(&msg), kMessageFilterCode))
    return true;
  WillProcessMessage(msg);
  uint32_t action = MessagePumpDispatcher::POST_DISPATCH_PERFORM_DEFAULT;
  if (state_->dispatcher)
    action = state_->dispatcher->Dispatch(msg);
  if (action & MessagePumpDispatcher::POST_DISPATCH_QUIT_LOOP)
    state_->should_quit = true;
  if (action & MessagePumpDispatcher::POST_DISPATCH_PERFORM_DEFAULT) {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
  DidProcessMessage(msg);
  return true;
}

bool MessagePumpForUI::ProcessPumpReplacementMessage() {
  bool have_message = false;
  MSG msg;
  if (MessageLoop::current()->os_modal_loop()) {
    // We only peek out WM_PAINT and WM_TIMER here for reasons mentioned above.
    have_message = PeekMessage(&msg, NULL, WM_PAINT, WM_PAINT, PM_REMOVE) ||
                   PeekMessage(&msg, NULL, WM_TIMER, WM_TIMER, PM_REMOVE);
  } else {
    have_message = PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) != FALSE;
  }
  int old_have_work = InterlockedExchange(&have_work_, 0);
  if (!have_message)
    return false;
  ScheduleWork();
  return ProcessMessageHelper(msg);
}

使用MessagePumpForIO處理IO任務時需要把IO事件句柄和完成處理方式IOHandler註冊到完成端口port_上,當有IO對象的操作完成時,MessageLoopForIO會調用IOHandler::OnIOCompleted方法處理IO完成事件。

void MessagePumpForIO::RegisterIOHandler(HANDLE file_handle,
                                         IOHandler* handler) {
  ULONG_PTR key = HandlerToKey(handler, true);
  HANDLE port = CreateIoCompletionPort(file_handle, port_, key, 1);
  DPCHECK(port);
}
bool MessagePumpForIO::RegisterJobObject(HANDLE job_handle,
                                         IOHandler* handler) {
  ULONG_PTR key = HandlerToKey(handler, false);
  JOBOBJECT_ASSOCIATE_COMPLETION_PORT info;
  info.CompletionKey = reinterpret_cast<void*>(key);
  info.CompletionPort = port_;
  return SetInformationJobObject(job_handle,
                                 JobObjectAssociateCompletionPortInformation,
                                 &info,
                                 sizeof(info)) != FALSE;
}

從chromium的消息循環的實現方式中,總結起來會發現有以下幾個優點:

1、機制簡單。chromium處理消息只有交換隊列時纔會有加鎖,任務都是封裝成task交到線程隊列中,從而避免多線程任務可能存在的同步、異步等許多問題。簡單來說A線程需要B線程做一些事情,然後回到A線程繼續做一些事情;在Chrome下你可以這樣來實現:生成一個Task,放到B線程的隊列中,在該Task的Run方法最後,會生成另一個Task,這個Task會放回到A的線程隊列,由A來執行。

2、分工明確,不同的線程處理不同性質的任務,UI線程處理窗口信號,IO線程處理IO事件,其它線程各自處理自己的任務,使線程能更快響應,避免UI線程被IO事件卡住這類問題。




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章