为了解决由于单消息队列而造成的队头阻塞问题,Chromium 团队从 2013 年到现在,花了大量的精力在持续重构底层消息机制。

1、第一次迭代:引入一个高优先级队列

首先在最理想的情况下,我们希望能够快速跟踪高优先级任务,比如在交互阶段,下面几种任务都应该视为高优先级的任务:

  • 通过鼠标触发的点击任务、滚动页面任务;
  • 通过手势触发的页面缩放任务;
  • 通过 CSS、JavaScript 等操作触发的动画特效等任务。

这些任务被触发后,用户想立即得到页面的反馈,所以我们需要让这些任务能够优先与其他的任务执行。要实现这种效果,我们可以增加一个高优级的消息队列,将高优先级的任务都添加到这个队列里面,然后优先执行该消息队列中的任务。最终效果如下图所示:

Chromium 是如何解决队头阻塞问题的_消息队列

我们使用了一个优先级高的消息队列和一个优先级低消息队列,渲染进程会将它认为是紧急的任务添加到高优先级队列中,不紧急的任务就添加到低优先级的队列中。然后我们再在渲染进程中引入一个任务调度器,负责从多个消息队列中选出合适的任务,通常实现的逻辑,先按照顺序从高优先级队列中取出任务,如果高优先级的队列为空,那么再按照顺序从低优级队列中取出任务。

将任务划分为多个不同的优先级,来实现更加细粒度的任务调度,比如可以划分为高优先级,普通优先级和低优先级,最终效果如下图所示:

Chromium 是如何解决队头阻塞问题的_消息队列_02

我们实现了三个不同优先级的消息队列,然后可以使用任务调度器来统一调度这三个不同消息队列中的任务。

现在我们引入了多个消息队列,结合任务调度器我们就可以灵活地调度任务了,这样我们就可以让高优先级的任务提前执行,采用这种方式似乎解决了消息队列的队头阻塞问题。

2、 第二次迭代:根据消息类型来实现消息队列

要解决上述问题,我们可以为不同类型的任务创建不同优先级的消息队列,比如:

  • 可以创建输入事件的消息队列,用来存放输入事件。
  • 可以创建合成任务的消息队列,用来存放合成事件。
  • 可以创建默认消息队列,用来保存如资源加载的事件和定时器回调等事件。
  • 还可以创建一个空闲消息队列,用来存放 V8 的垃圾自动垃圾回收这一类实时性不高的事件。

Chromium 是如何解决队头阻塞问题的_消息队列_03

通过迭代,这种策略已经相当实用了,但是它依然存在着问题,那就是这几种消息队列的优先级都是固定的,任务调度器会按照这种固定好的静态的优先级来分别调度任务。

3、第三次迭代:动态调度策略

根据实际场景来继续平衡这个跷跷板,也就是说在不同的场景下,根据实际情况,动态调整消息队列的优先级。一图胜过千言,我们先看下图:

Chromium 是如何解决队头阻塞问题的_消息队列_04

这张图展示了 Chromium 在不同的场景下,是如何调整消息队列优先级的。通过这种动态调度策略,就可以满足不同场景的核心诉求了,同时这也是 Chromium 当前所采用的任务调度策略。

上图列出了三个不同的场景,分别是加载过程,合成过程以及正常状态。当渲染进程接收到用户交互的任务后,接下来大概率是要进行绘制合成操作,因此我们可以设置,当在执行用户交互的任务时,将合成任务的优先级调整到最高。

接下来,处理完成 DOM,计算好布局和绘制,就需要将信息提交给合成线程来合成最终图片了,然后合成线程进入工作状态。现在的场景是合成线程在工作了,那么我们就可以把下个合成任务的优先级调整为最低,并将页面解析、定时器等任务优先级提升。

4、第四次迭代:任务饿死

不过依然存在一个问题,那就是在某个状态下,一直有新的高优先级的任务加入到队列中,这样就会导致其他低优先级的任务得不到执行,这称为任务饿死。Chromium 为了解决任务饿死的问题,给每个队列设置了执行权重,也就是如果连续执行了一定个数的高优先级的任务,那么中间会执行一次低优先级的任务,这样就缓解了任务饿死的情况。