React Fiber中的调度思想

希沃ENOW大前端前端

公司官网:CVTE(广州视源股份)算法

团队:CVTE旗下将来教育希沃软件平台中心enow团队浏览器

本文做者:markdown

王轩名片.png

前言

关于时间片分片逻辑,或许咱们大概都有所了解过,在React Fiber中,使用RequestIdelCallback(rIC)用来进行操做优化和时间分片。那么是否了解过具体是如何进行调度的?所谓的时分复用是什么?而这种调度思想是从哪发展而来的?对于咱们开发者而言,有什么是能够借鉴的吗?架构

咱们都知道在浏览器中,在主线程中,若是执行大量任务,会容易致使掉帧或者卡顿。而产生的缘由是在浏览器中使用VSync通知页面进行从新渲染,可是在JS的事件帧中,因为时间不够,致使任务阻塞从新渲染所致。人的眼睛大约每秒能够看到 60 帧,因此咱们通常将fps=60判断为用户体验是否优秀流畅的一个分水岭,通常fps<24的话,用户就会感到卡顿,由于人眼识别主要为24帧。分布式

VSync信号

当一帧画面绘制完成后,准备画下一帧前,显示器会发出一个垂直同步信号(vertical synchronization),简称 VSync。显示器一般以固定频率进行刷新,这个刷新率就是 VSync 信号产生的频率。oop

浏览器刷新率(帧) image.png 在浏览器中,一帧须要执行的任务有:布局

  1. 接受输入事件
  2. 执行事件回调
  3. 开始一帧
  4. 执行RAF(RequestAnimationFrame)
  5. 页面布局,样式计算
  6. 渲染
  7. 执行RIC(RequestIdelCallback)

所以若是存在任务运行时间过长,则会阻塞下一帧任务执行,就会形成卡顿和掉帧的现象。性能

那么在计算机的世界里,存在类似的现象吗?优化

单核

在现代计算机内,通常会有多核/多CPU架构存在。可是咱们但愿的是尽可能压榨核心的性能,那么不妨考虑下极限场景下的优化,或者是说模拟浏览器中JS执行单线程的操做:只存在单核如何处理多进程。

那么如何提供有许多CPU的假象呢?

时分共享

让一个进程只运行一个时间片,而后切换到其余进程,提供了存在多个虚拟CPU的假象,这种作法称为时分共享。即容许资源有一个实体使用一小段时间,而后有另外一个实体使用一小段时间,如此下去。

关注点分离

实际上,资源共享或者说切换进程须要消耗性能的,可是咱们能够先将关注点放在如何实现共享上,让关注点分离。 正如CAP原则中,在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼,一般只能取其二。可是因为咱们关注点的不一样,能够拆解开来,优先实现咱们所须要关注的。

固然,咱们能想到的最简单的方式实现就是相似于轮询。毫无心义的作切换工做,只要时间到了天然交给下一个。 单纯的时分共享其实是属于NOOB CODE。

image.png

咱们须要有更智能的策略——在操做系统内做出某种决定的算法。 首先:咱们对操做系统中运行的进程做出以下的假设:

  1. 每个工做运行相同的时间;
  2. 全部工做同时到达;
  3. 一旦开始,每一个工做都保持运行直到完成;
  4. 全部工做都只用CPU
  5. 每一个工做的运行时间是已知的。

而且咱们为此引入一个性能指标:周转时间,而且计算公式为 T(周转时间) = T(完成时间) - T(到达时间)

先进先出(FIFO)

假若有三个工做A、B、C,分别执行10s,那么从线性单任务的角度来看,平均周转时间即为(10 + 20 + 30) / 3 = 20s。

那么咱们不妨极端一点,位于后面的B、C任务分别执行1s,A任务执行了100s,那么整个的周转时间即为(100 + 110 + 120)/ 3 = 110s。

这个问题被称为护航效应:一些耗时较少的潜在资源被排在重量级的资源消费以后。 image.png

最短任务优先(SJF)

用时最短的任务优先执行,是否是就能解决这个问题了呢?

假如将上面的极端例子举例就会发现,平均周转时间变为(10 + 20 + 120)/ 3 = 50s。

image.png 在考虑全部任务同时到达的状况下,最短任务优先是最优的算法。可是在现实计算机世界中,咱们没法肯定下一个到达的任务是不是最短的任务,假若须要等待的话,那么花费的时间可能也会远低于其余算法。

那么假如咱们使用抢占式的方法会不会更好呢?

最短完成时间优先(STCF)

在第一个任务开始运行,后续的的两个任务到达,这时候咱们开始计算最短完成时间,而且将最短完成时间的任务调度到最前面执行。

平均周转时间为(10 + 20 + 120)/ 3 = 50s。

能够得知在任务中,抢占式的最短完成时间优先(STCF)算法能够得到较好的平均周转时间收益。

image.png

是的,基于系统而言,最短完成时间优先是一个很好的策略。然而对于用户而言,咱们的关注点应该是放在交互性上面。一样的,在前端中,咱们用户会更关心的是你的程序何时运行结束吗?更关心的应该是交互过程是否流畅。因此咱们须要一个新的度量标准:响应时间T(响应时间) = T(首次运行) - T(到达时间)。

基于新的度量标准,咱们会发现,对于最短完成时间优先算法并不友好:第三个任务必须等到前两个任务所有运行后才能运行。这对于用户体验来讲无疑是糟糕的。

那么,咱们如何构建对响应时间敏感的调度程序呢?

轮转(Round - Robin)

在一个时间片内运行一个任务,而后切换到运行队列中的下一个任务,而不是运行一个任务直到结束。操做系统的时间片长度是基于时钟中断(Timer Interrupt)的倍数而定。

时钟中断(Timer Interrupt)

从本质上说,时钟中断只是一个周期性的信号,彻底是硬件行为,该信号触发CPU去执行一个中断服务程序,可是为了方便,咱们就把这个服务程序叫作时钟中断。

image.png

以上面为例子: RR的平均响应时间为(0 + 1 + 2)/ 3 = 1s;SJF算法的平均响应时间是(5 + 10 + 15)/ 3 = 5s 在响应时间上RR具备更优秀的表现。

那么若是照上面的算法,是否是时间片越短越好呢,仍是以上图为例子,假如把时间片缩短到0.5,那么RR的平均响应时间就会是 (0 + 0.5 + 1)/ 3 = 0.5s !

是的,假如从理论上来讲,确实如此。不过放到实际中,在进程切换过程当中,其实是有切换成本的,所以咱们须要权衡时间片的长度,用来摊销上下文切换成本。

前面大体说了计算机中的进程调度,不妨回过头看下React Fiber在运行时的调度设计。

React Fiber中的调度

之前的React是线性执行任务,从原生执行栈递归遍历VDOM。在执行栈中压入和弹出任务,实际上就是前面说的先进先出的方式。

Stack Reconciler,是一个没法中断的方式 image.png 而新的调度方式Fiber Reconciler,则显得更为智能 image.png 在Fiber中,核心特性能够归纳为:

  1. 大型任务的中断和可拆解;
  2. 利用时间片作时间切割;
  3. 任务执行过程当中利用优先级的可抢占;

React Fiber的运行时实际上就是RequestIdelCallback(rIC)+ 优先级抢占(固然由于RequestIdelCallback取决于设备的Vsync信号发射频率,会形成不一样设备间的差别,所以优先使用polyfill,这个咱们暂时不展开细讲)。

在使用VSync信号进行分片的逻辑实际上跟时钟中断是同样,都是由硬件发出信号来指导逻辑触发。而后在每一个时间片上作任务的拆分和优先级的调度。

类比系统的进程调度就是轮转(RR)+ 最短完成时间优先(STCF),只是将最短完成时间优先替换为业务须要的优先级,在单个时间片内,寻找最优先级的抢占式调度。 ​

固然React Fiber自己还存在其余的优化策略,例如超时机制、任务的可中断,挂起,恢复、Concurrent模式等,咱们在次不一一展开讨论。

实际上,不管是轮转或者是React Fiber中的时间分片,完成任务执行时长都是大于最简单算法执行时长的(在不考虑I/O的状况下和其余优化的状况下),由于在切换或者计算过程当中会有消耗,可是基于关注点分离,咱们能够将关注点聚焦在咱们最迫切实现的功能上。

总结

由此,值得咱们借鉴的思考是:对于大型任务,咱们须要从自身的关注点出发,寻找相对合理的解决路径。能够进行合理的拆解,并使用更为智能的方式去执行单个的分解任务。并时刻站在巨人的肩膀上,看问题并寻找解决方案。

参考文章

  • 《操做系统导论 - 虚拟化CPU》
相关文章
相关标签/搜索