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》
相關文章
相關標籤/搜索