这篇文章会更加深入地讲解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事件卡住这类问题。