Chromium Base MessageLoop Internals (1)
class MessageLoop
Version: r70_3538
File: base/message_loop/message_loop.{h, cc}
A MessageLoop
is used to process events for a particular thread, i.e. the core infrastructure for implementing Communicating Sequential Process (CSP) model.
There is at most one MessageLoop instance on a thread.
MessageLoop
is a farily complex class, it driveds from multiple base classes:
1 | class BASE_EXPORT MessageLoop : public MessagePump::Delegate, |
MessageLoop Type
A MessageLoop has a particular type, differred by the set of asynchronous events it is capable of handling.
1 | // TYPE_DEFAULT |
The TYPE_CUSTOM
is recently added, IIRC.
Create a MessageLoop
The common usage of MessageLoop is as follows
1 | // Create a TYPE_DEFAULT message-loop. |
Although base::MessageLoop::Run()
is private, it is actually inherited from base::RunLoop::Delegate
, in which the function is public. So we simply skip base::RunLoop
related context here and just focus on base::MessageLoop
itself.
Let’s first check its constructor:
1 | using MessagePumpFactoryCallback = |
The ctor
- delegates the construction to another ctor
- calls
BindToCurrentThread()
and we know that pump factory callback must whatever returns a std::unique_ptr<MessagePump>()
instance, and for the message-loop in default-type, the factory callback is empty.
Now, we should better continue our construction-adventure, examing what the delegatee constructor does.
1 | MessageLoop::MessageLoop(Type type, MessagePumpFactoryCallback pump_factory) |
One of its bases, MessageLoopCurrent
is created, as we are creating a MessageLoop bound for the current thread.
Next few fancy members are actually underlying_task_runner
-centric; a MessageLoopTaskRunner
receives and queues tasks destined to its owning MessageLoop
, and this MessageLoop
will extract its tasks from the underlying_task_runner
.
So, we can think this underlying task runner is the source of tasks the message-loop demands.
message_loop_controller
, in MessageLoop::Controller
, is used by the uderlying-task-runner, to control its owning message-loop in some limited ways.
Note, DETACH_FROM_THREAD()
here has nothing to do with our topic, so we skip it too and turn our spotlight on BindToCurrentThread()
.
This function configures various members and binds this message loop to the current thread.
1 | void MessageLoop::BindToCurrentThread() { |
For what interests us, this function mainly
- creates the message-pump the loop is going to use.
- binds the instance to
MessageLoopCurrent
‘s TLS member - binds the underlying-task-runner to the thread
- starts scheduling via message-loop-controller
Then we see what does message_loop_controller_->StartScheduling()
do inside.
1 | void MessageLoop::Controller::StartScheduling() { |
Initially, is_ready_for_scheduling_
and pending_schedule_work_
are both false
, and the latter aren’t true
until MessageLoop::Controller::DidQueueTask()
is called.
Thus, this function is more like to declare that the message-loop is ready for scheduling/running; and, I guess, pending_schedue_work_
being true here should be rare and only in unusual cases.
Run a MessageLoop
From our example
1 | run_loop.Run(); |
is equivalent to
1 | base::MessageLoop loop; |
Let’s see it.
1 | void MessageLoop::Run(bool application_tasks_allowed) { |
Key point here is
1 | pump_->Run(this); |
and calling this function will ‘block’ on here, until the message-pump quits.
Similarly for MessageLoop::Quit()
, which forwards the call to MessagePump::Quit()
.
So I guess member pump_
‘s type, MessagePump
its our next big guy to trace.
class MessagePump
File: base/message_loop/message_pump*.{h, cc}
MessagePump
is an abstract/interface class and its whole hierarchy is fairly complex; and yet, it defines the basic routine of what MessagePump::Run()
should do.
1 | // The anatomy of a typical run loop: |
DoInternalDowrk()
is highly related with the actual pump we are using:
- for UI-pump, it receives and dispatches UI events from native system
- for IO-pump, it receives and notifies IO events
- but for Default-pump, it even does not provide this kind of function, because there is no such internal work for this kind of pump.
And for simplicity, we focus on the class MessagePumpDefault
this time.
The MessagePumpDefault
is simple and succinct, yet with full capabilities for normal message-loop uses.
Ctor and dtor of the class are both simple:
1 | MessagePumpDefault::MessagePumpDefault() |
event_
is a WaitableEvent
and used for controlling execution of the pump.
Now let us see the key function, Run()
:
1 | void MessagePumpDefault::Run(Delegate* delegate) { |
Some observations:
- no internal work for default pump
- if had work done, try to handle subsequent work immedately
- otherwise, block until being wakeup or for a given period (
delayed_work_time_
) - actual works are done by pump’s owner, the owning
MessageLoop
, viaMessagePump::Delegate
. - after execution of each kind of work, it checks if the loop should quit, which is indicated by
keep_running_
This loop model, doesn’t handles all pending tasks at once, it instead handles one task of a specific kind at one iteration, to make the whole loop more responsive.
By the way, quitting the pump-loop is simply to make keep_running_ = false
, which is exactly what MessagePump::Quit()
does:
1 | void MessagePumpDefault::Quit() { |
We now focus on MessageLoop
again, to examine how works are processed.
From code above, we know there are three categories of work:
- work
- delayed work
- idle work
Let’s go through one-by-one:
1 | bool MessageLoop::DoWork() { |
The function first checks to see if there is any pending task in the sequenced_task_source_
, then exracts it out and
- saves into the
pending_task_queue_.delayed_task
queue, if the task is a delayed-task - runs the pending task
Note, if the delayed task would expire earlier than any tasks in the delayed queue, the message-loop will reset expiration time.
This is done via MessagePump::ScheduleDelayedWork()
:
1 | void MessagePumpDefault::ScheduleDelayedWork( |
Delayed tasks in pending_task_queue_.delayed_task
will be processed subsequentely inside MessageLoop::DoDelayedWork()
:
1 | bool MessageLoop::DoDelayedWork(TimeTicks* next_delayed_work_time) { |
This function runs every tasks that has been expired.
Because we said before, that the pump handles a task at a time, so the above code contains a minor optimization for handling delayed takss: it calls TimeTicks::Now()
only when next_run_time
maybe in the future.
Doing so can reduce number of times it calls Time::Now()
.
Function MessageLoop::DoIdleWork()
is rather boring, it does some metrics work only.
Post Tasks
Two frequently used functions for adding a task to the message-loop are:
- PostTask()
- PostDelayedTask()
and in fact, PostTask()
is merely a call of PostDelayedTask()
with 0-delay.
And in our example, this function is implemented by class MessageLoopTaskRunner
.
For brevity, I deleted some not-so-related code on purpose:
1 | bool MessageLoopTaskRunner::PostDelayedTask(const Location& from_here, |
We can see that, if a task is a delayed task, its time-detal will be converted into an absolute time-tick.
Then the task is enqueued into incoming_queue_
, which is
1 | using TaskQueue = base::queue<PendingTask>; |
Insertion is done with a lock being hold, because the task may come from other threads.
Finally it notifies a task was queued, to let the pump reschedule if necessary.
Retrieval of tasks out of the runner is not so complicated too:
1 | // Tasks to be returned to TakeTask(). Reloaded from |incoming_queue_| when |
HasTasks()
has major side-effects, it reloads outgoing_queue_
from incoming_queue_
when necessary; while TaskTask()
simply extracts a task from the outgoing_queue_
.
NOTE: accesses to outgoing_queue_
is thread-safe without locks, because we only dequeue tasks on the thread running the message-loop.
Using two queues here, is to reduce contention.
For completeness, we now take a look at pending_task_queue_.delayed_tasks
, which stores timed-tasks.
1 | DelayedQueue delayed_tasks_; |
um…it is a simple priority queue.