這篇文章會更加深入地講解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事件卡住這類問題。