⚙️ Chromium 消息循环

从 MessagePump 到 SequenceManager 的主线程调度引擎

基于 Chromium 源码深度解析
2026-03-12 | 技术深度解读

目录

Part 1
  • 项目背景与问题域
  • 总体架构
  • 核心抽象
  • 关键类解析
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

场景差异
  • UI 输入
  • 渲染提交
  • 网络回调
  • 空闲任务
调度手段
  • 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、两套取消语义,后面很难收敛。

源码阅读建议

  1. 先看 message_pump.h 契约
  2. 再看 message_pump_default.cc 理解最小实现
  3. 然后看 thread_controller_with_message_pump_impl.cc
  4. 最后啃 sequence_manager_impl.cctask_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.h
  • base/message_loop/message_pump_default.cc
  • base/message_loop/message_pump_epoll.cc
  • base/task/sequence_manager/thread_controller_with_message_pump_impl.cc
  • base/task/sequence_manager/sequence_manager_impl.cc
  • base/task/sequence_manager/task_queue_impl.cc
  • base/run_loop.cc

读完这些,再去看 Blink scheduler / renderer main thread,会顺很多。