⚙️ Chromium 消息循环
从 MessagePump 到 SequenceManager 的主线程调度引擎
基于 Chromium 源码深度解析
2026-03-12 | 技术深度解读
目录
Part 2- 调度流程
- 嵌套 RunLoop
- Epoll / WakeUp
- 性能与最佳实践
Chromium 为什么需要消息循环
浏览器主线程既要处理 UI / IPC / IO 事件,又要执行 posted task、定时任务、延迟任务和嵌套 loop。单纯 while(true) + queue 远远不够。
- 跨平台:Windows / Linux / macOS / Android 统一抽象
- 低延迟:有任务就尽快唤醒,没有任务就安全休眠
- 可嵌套:弹窗、同步等待、原生消息泵都可能进入 nested loop
- 可控公平性:不同 TaskQueue 需要优先级和 fence
源码地图
事件泵层- message_pump.h
- message_pump_default.cc
- message_pump_epoll.cc
调度层- thread_controller_with_message_pump_impl.*
- sequence_manager_impl.*
- task_queue_impl.*
- run_loop.*
理解顺序:Pump → ThreadController → SequenceManager → TaskQueue → RunLoop
总体分层
Application / Browser Thread
↓ PostTask / PostDelayedTask
TaskQueueImpl (多队列 + fence + observer)
↓ selector / wake-up queue
SequenceManagerImpl (统一调度器)
↓ SequencedTaskSource
ThreadControllerWithMessagePumpImpl
↓ MessagePump::Delegate
MessagePumpDefault / MessagePumpEpoll / UI Pump
↓ OS wait primitive (eventfd / epoll / poll / native loop)
核心循环契约
MessagePump 不关心“任务语义”,只负责一个稳定契约:反复调用 Delegate::DoWork / DoIdleWork,并在需要时被 ScheduleWork 或 Delay 唤醒。
DoWork():尝试执行应用任务,返回下一次唤醒信息DoIdleWork():进入休眠前的收尾操作ScheduleWork():跨线程 or 同线程立即唤醒ScheduleDelayedWork():为最早 deadline 编排等待
MessagePump 接口
class MessagePump {
public:
class Delegate {
public:
struct NextWorkInfo {
TimeTicks delayed_run_time;
bool is_immediate() const;
};
virtual NextWorkInfo DoWork() = 0;
virtual void DoIdleWork() = 0;
virtual void BeforeWait() = 0;
virtual void BeginNativeWorkBeforeDoWork() = 0;
};
virtual void Run(Delegate* delegate) = 0;
virtual void Quit() = 0;
virtual void ScheduleWork() = 0;
virtual void ScheduleDelayedWork(...)=0;
};
NextWorkInfo 的意义
返回值不是“是否执行了任务”,而是“下一步该多快再调一次 DoWork”。这让 Pump 可以精准控制睡眠/忙等策略。
- immediate:马上再进 DoWork,常见于队列中还有 ready task
- delayed_run_time:有未来任务,睡到 deadline 或被外部唤醒
- max / null:当前没有明确 deadline,可以更保守休眠
MessagePump::Create
std::unique_ptr<MessagePump> MessagePump::Create(MessagePumpType type) {
switch (type) {
case MessagePumpType::UI: return std::make_unique<MessagePumpForUI>();
case MessagePumpType::IO: return std::make_unique<MessagePumpForIO>();
case MessagePumpType::DEFAULT: return std::make_unique<MessagePumpDefault>();
...
}
}
同一套上层调度逻辑,通过工厂切到不同平台实现。
对齐唤醒策略
message_pump.cc 里有 AdjustDelayedRunTime() 与 kAlignWakeUps。目标不是更快,而是更省电:把相近 deadline 合并到一起。
- 减少频繁 timer interrupt
- 移动端/电池场景更重要
- 对实时性要求高的任务则保留精确唤醒
MessagePumpDefault 主循环
for (;;) {
Delegate::NextWorkInfo next = delegate->DoWork();
bool immediate = next.is_immediate();
if (state_->should_quit) break;
if (immediate) continue;
delegate->DoIdleWork();
delegate->BeforeWait();
event_.TimedWait(next.remaining_delay());
}
这是最纯粹的“任务驱动 + 条件变量/事件等待”模型。
Busy Loop 优化
MessagePumpDefault 不是永远 sleep。对超短等待窗口,它会短暂 busy-wait,避免频繁上下文切换造成的尾延迟。
- 适合极短 deadline
- 换来更稳定的任务响应时间
- 但 Chromium 明确限制 busy loop 时长,避免电量浪费
MessagePumpEpoll 的角色
Linux 上的 Pump 不只跑任务,还能接 native fd 事件。eventfd 负责自唤醒,epoll/poll 负责 IO readiness。
- 统一处理 task + fd event
- 支持 one-shot / persistent watch
- 将“原生事件”纳入同一个执行节拍
WatchFileDescriptor
bool MessagePumpEpoll::WatchFileDescriptor(int fd,
bool persistent,
Mode mode,
FdWatchController* controller,
FdWatcher* delegate)
这层把 epoll interest、controller 生命周期、回调 delegate 绑定到一起,避免上层直接碰 epoll_ctl 细节。
Epoll Run 循环
for (;;) {
NextWorkInfo next = delegate->DoWork();
if (state->should_quit) break;
bool did_native_work = WaitForEpollEvents(timeout);
if (immediate || did_native_work) continue;
delegate->DoIdleWork();
}
和 Default pump 很像,但中间多了一步 WaitForEpollEvents。
HandleWakeUp
ScheduleWork() 最终会写入 wakeup fd;epoll 返回后,Pump 识别这个特殊 fd,并清空事件,保证 DoWork 立刻被再次调用。
- 跨线程 PostTask 能及时唤醒主循环
- 避免 signal/pipe 方案的复杂性
- 与 epoll 统一进一个等待集合
原生事件批处理
message_pump_epoll.cc 里有 kBatchNativeEventsInMessagePumpEpoll 开关。Chromium 会权衡:一次处理更多 native events,减少频繁在 native work / DoWork 之间切换。
这是一个典型吞吐与公平性平衡点。
RunLoop 的定位
RunLoop 不是底层泵,而是面向上层 API 的“同步等待壳”。它依赖当前线程已注册的 Delegate,允许代码临时进入一个受控的嵌套循环。
Run()RunUntilIdle()Quit() / QuitWhenIdle()
RunLoop 类型
enum class Type {
kDefault,
kNestableTasksAllowed,
};
- kDefault:嵌套时尽量只跑系统任务,避免重入风险
- kNestableTasksAllowed:允许应用任务在 nested loop 中继续执行
RunLoop::Run
void RunLoop::Run(...) {
if (!BeforeRun()) return;
const bool application_tasks_allowed =
run_depth == 1 || type_ == Type::kNestableTasksAllowed;
delegate_->Run(application_tasks_allowed, TimeDelta::Max());
AfterRun();
}
关键点:真正跑循环的是当前线程的 Delegate,而不是 RunLoop 自己。
QuitClosure 设计
RunLoop 提供 QuitClosure() / QuitWhenIdleClosure(),并通过 ProxyToTaskRunner 保证跨线程调用时能安全回到所属线程。
这是一种很稳的线程亲和性封装。
ThreadControllerWithMessagePumpImpl 的职责
向下- 实现 MessagePump::Delegate
- 驱动 pump Run/Quit/Schedule
向上- 从 SequencedTaskSource 拿任务
- 把 deadline 翻译成 ScheduleDelayedWork
- 处理 batch、嵌套、默认 TaskRunner
ThreadController 关键接口
void ScheduleWork() override;
void SetNextDelayedDoWork(LazyNow*, std::optional<WakeUp>);
MessagePump::Delegate::NextWorkInfo DoWork() override;
void DoIdleWork() override;
void BeginNativeWorkBeforeDoWork() override;
它是 Chromium 消息循环真正的“翻译层”。
ScheduleWork 去重
ThreadController 内部借助 WorkDeduplicator 判断是不是已经安排过 work。这样频繁 PostTask 不会每次都打醒底层 pump。
SetNextDelayedDoWork
if (wake_up) {
run_time = pump_->AdjustDelayedRunTime(...);
pump_->ScheduleDelayedWork({run_time, leeway, lazy_now->Now()});
}
SequenceManager 计算“下一个任务何时该跑”,ThreadController 再把它翻译成 Pump 能理解的定时唤醒。
DoWork 主体
DoWork() 做三件事:拉取 ready task、执行一批任务、决定下次唤醒时间。
- 如果又有 immediate work:返回 immediate
- 如果只有未来 deadline:返回 delayed_run_time
- 如果 idle:交给 DoIdleWork / BeforeWait 流程
Batch 执行模型
ThreadController 维护 work_batch_size。一次 DoWork 可执行多条任务,减少回到 pump / native loop 的切换成本。
- 吞吐更高
- 过大则影响公平性与输入响应
- Chromium 允许动态开关/调整
嵌套 RunLoop 处理
void OnBeginNestedRunLoop() override;
void OnExitNestedRunLoop() override;
int RunDepth() override;
ThreadController 需要知道当前 run depth,因为 nested loop 会改变任务是否允许执行、什么时候该 yield,以及 quit 信号该退出哪一层。
BeforeWait 钩子
在真正睡眠前,Delegate::BeforeWait() 有一次最后整理机会。Chromium 用它修正去重状态,避免“以为没人调度,但其实刚有任务入队”的竞态。
SequenceManagerImpl 的定位
SequenceManager 是 Chromium 调度体系的大脑:它拥有多个 TaskQueue,维护优先级、fence、wake-up queue、task observer,并决定下一条该跑谁。
SequenceManager 关键方法
void BindToMessagePump(std::unique_ptr<MessagePump> pump) override;
void ScheduleWork();
std::optional<WakeUp> GetPendingWakeUp(...);
void DidRunTask(LazyNow& lazy_now) override;
bool OnIdle() override;
BindToMessagePump
manager->BindToMessagePump(std::move(message_pump));
controller_->BindToCurrentThread(std::move(pump));
controller_->SetSequencedTaskSource(this);
绑定后,SequenceManager 不再直接碰底层等待原语,而是只和 ThreadController 说话。
为什么要多 TaskQueue
调度手段- priority
- fence
- throttler
- wake-up policy
TaskQueueImpl 四层队列
Immediate Incoming Queue → 接收刚 post 的立即任务
Delayed Incoming Queue → 小根堆/延迟任务
Immediate Work Queue → 进入当前轮次可执行的任务
Delayed Work Queue → 已到期后转移过来的延迟任务
这种拆分能把并发入队、ready 判定、执行消费解耦。
PostImmediateTaskImpl
void PostImmediateTaskImpl(PostedTask task, ...) {
push into immediate_incoming_queue;
if (should_schedule_work)
sequence_manager_->ScheduleWork();
}
重点不在“塞进队列”,而在:只有从空到非空等关键状态变化时才触发 ScheduleWork。
PostDelayedTaskImpl
延迟任务先进 delayed queue,并通过 UpdateWakeUp() 与 SequenceManager 的 wake-up queue 协调。真正执行前,需要先转移到 work queue。
MoveReadyDelayedTasksToWorkQueue
到期的延迟任务不会直接跑,而是先移动到 work queue,再参与统一公平调度。这样 immediate task 与 delayed task 共享一条执行管道。
Fence 机制
void InsertFence(...);
void InsertFenceAt(TimeTicks time);
void RemoveFence();
bool BlockedByFence() const;
Fence 相当于“停止线”,能让某个 queue 暂时不可执行,常用于生命周期切换、页面冻结、导航隔离。
Queue Priority
TaskQueueSelector 会综合优先级与 ready 状态选择下一个 queue。不是所有任务都 FIFO 平权;输入响应和关键 UI 更新通常要压过低优先级后台活。
Observer 与埋点
void AddTaskObserver(TaskObserver* observer);
void SetOnTaskStartedHandler(...);
void SetOnTaskCompletedHandler(...);
void AddTaskTimeObserver(TaskTimeObserver* observer);
Chromium 把可观测性内建在调度器里,方便跟踪 jank、长任务、队列饥饿。
Task 执行回调
TaskQueueImpl::OnTaskStarted / OnTaskCompleted 会围绕任务执行做通知、采样、trace。这样统计能力不需要业务层自己散落埋点。
RunOrPostTask
if (RunsTasksInCurrentSequence()) {
RunTaskSynchronously(...);
} else {
PostTask(...);
}
这是 Chromium 里很实用的一个模式:如果已经在正确线程,直接同步跑,省掉一次循环往返。
默认 TaskRunner 句柄
ThreadController 会为当前线程安装 SingleThreadTaskRunner::CurrentDefaultHandle。这样大量不显式持有 runner 的代码,仍能通过 thread-local 默认 runner 投递任务。
一次任务完整路径
PostTask()
→ TaskQueueImpl::PostImmediateTaskImpl
→ SequenceManagerImpl::ScheduleWork
→ ThreadController::ScheduleWork
→ MessagePump::ScheduleWork
→ wakeup fd / event signaled
→ Pump::Run() 唤醒
→ Delegate::DoWork
→ SequenceManager 选择 ready queue
→ TaskQueueImpl 弹出任务并执行
延迟任务路径
PostDelayedTask()
→ TaskQueue delayed incoming heap
→ UpdateWakeUp()
→ SequenceManager 计算最早 WakeUp
→ ThreadController::SetNextDelayedDoWork
→ Pump::ScheduleDelayedWork
→ timeout 到达
→ MoveReadyDelayedTasksToWorkQueue
→ DoWork 执行
Native Event 与 Task 交织
Epoll pump 会在 native fd event 与 application task 之间穿插处理。Chromium 用 BeginNativeWorkBeforeDoWork() 告诉上层:“刚发生原生工作,下一轮可能要更积极地跑 DoWork”。
UML:类关系
RunLoop ──uses──▶ RunLoop::Delegate
▲
│ implemented by
ThreadControllerWithMessagePumpImpl ──implements──▶ MessagePump::Delegate
▲
│ owns/consults
SequenceManagerImpl ──owns──▶ TaskQueueImpl*
│
└──binds to──▶ MessagePumpDefault / MessagePumpEpoll
时序图:立即任务
Poster -> TaskQueueImpl: PostTask
TaskQueueImpl -> SequenceManager: ScheduleWork
SequenceManager -> ThreadController: ScheduleWork
ThreadController -> MessagePump: ScheduleWork
MessagePump -> ThreadController: DoWork
ThreadController -> SequenceManager: Select next task
SequenceManager -> TaskQueueImpl: TakeTask
TaskQueueImpl -> Runnable: Run()
时序图:延迟任务
Poster -> TaskQueueImpl: PostDelayedTask
TaskQueueImpl -> SequenceManager: UpdateWakeUp
SequenceManager -> ThreadController: SetNextDelayedDoWork
ThreadController -> Pump: ScheduleDelayedWork(t)
... wait until t ...
Pump -> ThreadController: DoWork
ThreadController -> SequenceManager: GetPendingWakeUp / ready queues
SequenceManager -> TaskQueueImpl: MoveReadyDelayedTasksToWorkQueue
时序图:嵌套循环
Task A -> RunLoop::Run()
RunLoop -> ThreadController::Run(application_tasks_allowed?)
ThreadController -> Pump::Run(delegate)
Nested native loop occurs
ThreadController::RunDepth increases
Quit() only exits topmost active loop
Outer loop resumes after nested loop unwinds
为什么这套设计可靠
- 分层清晰:Pump 不懂任务语义,Queue 不懂 OS 等待原语
- 契约稳定:一切围绕 DoWork / ScheduleWork / delayed wake-up
- 嵌套可控:RunDepth、quit_pending、nestable policy 明确化
- 观测完善:task observer / trace / metrics 一开始就内建
性能优化点 1:减少唤醒
- ScheduleWork dedup
- AlignWakeUps 合并接近的 timer
- 只有关键状态变化才 signal eventfd
减少“空唤醒”是浏览器主线程省电与稳定性的基本功。
性能优化点 2:批处理
- DoWork 一次跑多个 task
- Batch native events
- 减少 task/native/task/native 抖动
批处理提升吞吐,但要防止长批次拖慢输入响应。
性能优化点 3:时间抽象
LazyNow 避免在一次调度循环里重复取系统时间;WakeUp / TimeDomain 则把 deadline 计算集中管理,降低时间判断散落在各处的成本。
常见反模式
- 在主线程里滥开 nested RunLoop
- 把低优先级大任务塞进高优先级队列
- 频繁 PostTask 但不做 dedup / coalescing
- 忽略 fence 与生命周期,导致页面冻结后仍跑活
- 用同步等待掩盖异步设计问题
工程启示 1
如果你在写桌面端、移动端或复杂前端宿主,最值得学的不是 epoll 细节,而是 层次隔离:等待、调度、队列、业务不要耦死。
工程启示 2
一个成熟消息循环一定要同时满足四件事:快、稳、省、可观测。只追求低延迟而忽略省电和观测,迟早会在真实设备上翻车。
工程启示 3
“立即任务”和“延迟任务”最好共用一条最终执行通道。否则就会出现两套公平性规则、两套 trace、两套取消语义,后面很难收敛。
源码阅读建议
- 先看
message_pump.h 契约 - 再看
message_pump_default.cc 理解最小实现 - 然后看
thread_controller_with_message_pump_impl.cc - 最后啃
sequence_manager_impl.cc 和 task_queue_impl.cc
关键源码片段 1:ScheduleWork
void ThreadControllerWithMessagePumpImpl::ScheduleWork() {
if (work_deduplicator_.OnWorkRequested() ==
ShouldScheduleWork::kScheduleImmediate) {
pump_->ScheduleWork();
}
}
一句话看懂:请求 work ≠ 总要唤醒 pump。
关键源码片段 2:RunLoop Quit 代理
if (!origin_task_runner_->RunsTasksInCurrentSequence()) {
origin_task_runner_->PostTask(FROM_HERE,
BindOnce(&RunLoop::Quit, Unretained(this)));
return;
}
线程亲和性不是文档约定,而是代码层面强制执行。
关键源码片段 3:TaskQueue 入队后调度
bool should_schedule_work = false;
... push task into incoming queue ...
if (should_schedule_work)
sequence_manager_->ScheduleWork();
入队和唤醒拆开,是为了把锁范围、状态变更和昂贵动作分离。
总结
一句话Chromium 的消息循环,本质是一套以 DoWork 契约为中心的跨平台任务调度内核。
你应该记住- Pump 负责等待
- ThreadController 负责翻译
- SequenceManager 负责决策
- TaskQueue 负责局部策略
扩展阅读
base/message_loop/message_pump.hbase/message_loop/message_pump_default.ccbase/message_loop/message_pump_epoll.ccbase/task/sequence_manager/thread_controller_with_message_pump_impl.ccbase/task/sequence_manager/sequence_manager_impl.ccbase/task/sequence_manager/task_queue_impl.ccbase/run_loop.cc
读完这些,再去看 Blink scheduler / renderer main thread,会顺很多。