React18原理: Fiber架构下的单线程CPU调度策略
概述
- React 的 Fiber 架构, 它的整个设计思想就是去参考CPU的调度策略
- CPU现在都是多核多进程的,重点研究的是
- CPU是单核单线程,它是如何调度的?
- 为什么要去研究单线程的CPU?
- 浏览器中的JS它是单线程的
- JS 的执行线程和浏览器的渲染GUI 是互斥的
- 渲染和JS的执行都用同一个线程,因为一次只能做一件事情,所以互斥
- 所以,React整个架构的整个调度都是去参考 CPU 的
单线程CPU调度策略
单处理器进程调度策略
-
1 ) 先到先得(First-Come-First-Served,FCFS)
- 可以把 CPU 理解为一个办事窗口, 比如说排队,有的人办的慢,有的人办的快
- 比如,有人办事只需要一分钟,有的人却很墨迹,办事要半个小时
- 办事墨迹的人,后面的人都一直在等着,所以这个先到先得的策略
- 对于CPU来讲,其实就是不合理的,不能谁先来就先执行
- 因为有的人比较着急(优先级高),放在后面不适合
- 这是最简单的调度策略,简单说就是没有调度,谁先来谁就先执行
- 执行完毕后就执行下一个,不过如果中间某些进程因为I/O阻塞了
- 这些进程可能会重新排队了,需要留意的事:
- FCFS对短进程不利
- 短进程即执行时间非常短的进程
- 可以用饭堂排队来比喻:
- 在饭堂排队打饭的时候,一个人打包好几份
- 这些人就像长进程一样,霸占着CPU资源
- 后面排队只打一份的人会觉得很吃亏
- 打一份的人会觉得它们优先级应该更高,毕竟它们花的时间很短
- 反正你打包那么多份再等一会也是可以的,何必让后面那么多人等这么久…
- FCFS对I/0密集不利
- I/O密集型进程(这里特指同步I/O)在进行I/O操作时
- 会阻塞休眠,这会导致进程重新被放入就绪队列,等待下一次
- 可以类比银行部门办业务:假设CPU一个窗口、I/O一个窗口
- 在CPU窗口好不容易排到你了,这时候发现一个不符合条件或者漏办了
- 需要去 I/O 搞一下,要去I/O窗口排队,I/O执行完了
- 到CPU窗口又得重新排队,这样,似乎也是不合理的
- 所以FCFS这种原始的策略在单处理器进程调度中并不受欢迎
-
2 )轮转调度
- 轮转调度它是基于时钟的一种抢占策略
- CPU的时钟其实就是根据它的这个帧幅周期
- 这个就跟吃大锅饭一样,也没有体现一种权重
- 最后, 比如说有人有急事,那这个时候搞轮转,也是不合适的
- 它属于抢占策略中最简单的一种:
- 公平地给每一个进程一定的执行时间,当时间消耗完毕或阻塞
- 操作系统就会调度其他进程,将执行权抢占过来
- 决策模式:
- 抢占策略相对应的有非抢占策略
- 非抢占策略指的是让进程运行直到结束、阻塞(如I/O或睡眠)
- 或者主动让出控制权,抢占策略支持中断正在运行的进程
- 将主动权掌握在操作系统这里,不过通常开销会比较大
- 这种调度策略的要点是
- 确定合适的时间片长度:太长了,长进程霸占太久资源
- 其他进程会得不到响应(等待执行时间过长)
- 这时候就跟上述的FCFS没什么区别了
- 太短了也不好,因为进程抢占和切换都是需要成本的
- 而且成本不低,时间片太短,时间可能都浪费在上下文切换上了
- 导致进程干不了什么实事
- 因此时间片的长度最好符合大部分进程完成一次典型交互所需的时间
- 轮转策略非常容易理解,只不过确定时间片长度需要伤点脑筋
- 另外和FCFS一样,轮转策略对I/O进程还是不公平
-
3 )最短进程优先(Shortest Process Next,SPN)
- 什么是短进程呢?
- 短进程就跟排队一样,比如,我办事一分钟,我就是短进程
- 你墨迹半个小时,你就是长进程
- 所以,可以设计一种调度策略叫短进程优先
- 如果说,我办的快,我就优先,那慢的一直在后面等着
- 这样好像也有点不对劲,所以这个也有缺陷
- 长进程就会饥饿,可能永远得不到响应,没法去执行, 所以,这个也不行
- 上面说了先到先得策略对短进程不公平
- 最短进程优先索性就让最短的进程优先执行
- 也就是说:按照进程的预估执行时间对进程进行优先级排序
- 先执行完短进程,后执行长进程,这是一种非抢占策略
- 这样可以让短进程能得到较快的响应
- 但怎么获取或者评估进程执行时间呢?
- 一是让程序的提供者提供,这不太靠谱
- 二是由操作系统来收集进程运行数据,并对它们进程统计分析
- 例如最简单的是计算它们的平均运行时间
- 不管怎么说都比上面两种策略要复杂一点
- SPN的缺陷是:
- 如果系统有大量的短进程,那么长进程可能会饥饿得不到响应
- 另外因为它不是抢占性策略,尽管现在短进程可以得到更多的执行机会
- 但是还是没有解决FCFS的问题:一旦长进程得到CPU资源
- 得等它执行完,导致后面的进程得不到响应
-
其他策略
- 最短余(Shortest Remaining Time,SRT)
- 最高响应比优先(HRRN)
- 反馈法
- 这里不再举例
前端框架解决上述问题的方向
- 对于前端框架来说,解决这种问题有三个方向:
- 1 )优化每个任务,让它有多快就多快。挤压CPU运算量
- Vue 选择的是这一种,对于Vue来说,使用模板让它有了很多优化的空间
- Vue使用依赖收集,基于Proxy来监听数据的变化,在get时收集依赖,在set时
- 触发依赖渲染页面,配合响应式机制可以让Vue精确地进行节点更新
- 2 )快速响应用户,让用户觉得够快,不能阻塞用户的交互(React分片)
- React选择了这一种,一个任务,假如说要运行1秒钟
- 那React通过分片之后,可能用了1.2秒,但是它却让你感觉很快
- 实际上它的执行时间是没有降低的,但是它会让你感觉快
- React 的 Reconcilatin 是 CPU 密集型的操作,它就相当于我们上面讲的
- 长进程,所以初衷和进程调度一样,要让高优先级的进程或短进程优先运行
- 不能让长进程长期霸占资源
- 3 )尝试Worker多线程
- 要保证状态和视图的一致性相当麻烦,这里不再赘述
- 1 )优化每个任务,让它有多快就多快。挤压CPU运算量
React 不同模式的效果对比
1 )同步模式下的 React, 可以看到,阻塞比较严重
2 ) 异步 Concurrent 模式下的 React, 可以看到,非常流畅
React是怎么优化的
- 为了给用户制造一种应用很快的假象,不能让一个程序长期霸占着资源
- 可以将浏览器的渲染、布局、绘制、资源加载(例如HTML解析)、事件响应、脚本执行视作操作系统的进程
- 需要通过某些调度策略合理地分配CPU资源,从而提高浏览器的用户响应速率,同时兼顾任务执行效率
- 所以React通过Fiber架构,让 Reconcilation 过程变成可被中断
- 适时地让出CPU执行权,让浏览器及时地响应用户的交互