フレームワーク作者の視点から:Reactスケジューリングアルゴリズムの反復プロセス

フレームワーク作者の視点から:Reactスケジューリングアルゴリズムの反復プロセス

みなさんこんにちは、カソンです。

React 内で最も理解しにくい部分は「スケジューリング アルゴリズム」です。これは抽象的で複雑なだけでなく、一度リファクタリングする必要があります。

このアルゴリズムを完全に理解できるのは、React チーム自身だけだと言えます。

今回は、React チーム メンバーの観点から「スケジューリング アルゴリズム」について説明してみたいと思います。

スケジューリングアルゴリズムとは何ですか?

React が v16 より前に直面していた主なパフォーマンスの問題は、コンポーネント ツリーが大きい場合にステータスを更新するとページがフリーズする可能性があることでした。根本的な原因は、更新プロセスが「同期的で中断不可能」だったことです。

この問題を解決するために、React は「更新プロセスを非同期かつ中断可能にする」ことを目的とした Fiber アーキテクチャを提案しました。

最終的な対話プロセスは次のとおりです。

  1. 異なるインタラクションは異なる優先度の更新を生成します (たとえば、onClick コールバックの更新は最高の優先度を持ちますが、useEffect コールバックでトリガーされる更新は平均的な優先度を持ちます)
  2. 「スケジューリングアルゴリズム」は、このレンダリングの優先度として多くの更新から優先度を選択します。
  3. 手順2で選択した優先度でコンポーネントツリーをレンダリングします。

レンダリング プロセス中に、インタラクション プロセスが再度トリガーされ、ステップ 2 でより高い優先度が選択されると、以前のレンダリングが中断され、新しい優先度でレンダリングが再開されます。

この記事では、ステップ 2 の「スケジューリング アルゴリズム」について説明します。

有効期限スケジュールアルゴリズム

スケジューリング アルゴリズムが解決する必要がある最も基本的な問題は、多数の更新の中から 1 つの更新の優先度をこのレンダリングの優先度として選択する方法です。

最も古いアルゴリズムは、expirationTime アルゴリズムと呼ばれます。

具体的には、更新の優先度は、「インタラクションをトリガーする現在の時刻」と「優先度に対応する遅延時間」に関連しています。

  1. // MAX_SIGNED_31_BIT_INTは最大31ビット整数です
  2. update.expirationTime = MAX_SIGNED_31_BIT_INT - (現在の時間 + 更新優先度);

たとえば、高優先度更新 u1 と低優先度更新 u2 の updatePriority はそれぞれ 0 と 200 です。

  1. MAX_SIGNED_31_BIT_INT - (現在の時刻 + 0) > MAX_SIGNED_31_BIT_INT - (現在の時刻 + 200)
  2.  
  3. // 今すぐ
  4. u1.有効期限 > u2.有効期限;

つまり、u1 の方が優先度が高いということです。

有効期限アルゴリズムの原理は簡単に理解できます。毎回、すべての更新の中から「最高の優先度」が選択されます。

「バッチ」の表現方法

さらに、解決する必要がある別の問題があります。それは、「バッチ」をどのように表現するかということです。

バッチとは何でしょうか? 次の例を考えてみましょう。

  1. // 状態番号を定義する
  2. 定数[num, updateNum] = useState(0);
  3.  
  4. // ...numが変更される場所
  5. // 変更方法1
  6. 更新番号(3);
  7. // 変更方法2
  8. 数値を1に更新します。

どちらの「状態を変更する方法」でも更新が作成されますが、違いは次のとおりです。

  • 最初の方法は、更新前のステータスを考慮せずにステータス番号を 3 に変更することです。
  • 2番目の方法では、「更新前の状態」に基づいて新しい状態を計算する必要があります。

2 番目の方法のおかげで、更新間の継続性が確保されます。

したがって、「スケジューリング アルゴリズム」が優先度を計算した後、コンポーネントがレンダリングされるときに「現在の状態値」の計算に実際に参加するのは次のコンポーネントです。

「計算された優先度を更新する」+「この優先度に関連する他の優先度を更新する」

これらの相互に関連する連続的な更新は「バッチ」と呼ばれます。

ExpirationTime アルゴリズムは、単純かつ大まかな方法​​で「バッチ」を計算します。つまり、特定の値 (priorityOfBatch) よりも高い優先度を持つ更新が同じバッチにグループ化されます。

  1. 定数 isUpdateIncludedInBatch = priorityOfUpdate >= priorityOfBatch;

expireTime アルゴリズムにより、レンダリングが非同期的に中断可能になり、優先度が最も高い更新が常に最初に処理されることが保証されます。

この期間中、この機能は非同期モードと呼ばれていました。

IO集約型のシナリオ

非同期モードは次の問題を解決できます。

  1. 複雑なコンポーネント ツリー ロジックにより、更新時に遅延が発生します (コンポーネントのレンダリングが中断されるため)
  2. 重要なやり取りはより速く応答します(異なるやり取りからの更新には異なる優先順位があるため)

これらの問題は総称して CPU バウンド問題と呼ばれます。

フロントエンドでは、エクスペリエンスに影響を与える別の種類の問題、つまり「データの要求によって発生する待機」があります。この種の問題は IO バウンド問題と呼ばれます。

IO 集約型の問題を解決するために、React は Suspense を提案しました。次のコードを考えてみましょう。

  1. 定数App = () => {
  2. 定数[ count ,setCount] = useState(0);
  3.    
  4. 使用効果(() => {
  5. 定数t = setInterval(() => {
  6. setCount(カウント=>カウント+ 1);
  7. }, 1000);
  8. 戻り値() => clearInterval(t);
  9. }, []);
  10.    
  11. 戻る
  12. <>
  13. <Suspense fallback={<div>読み込み中...</div>}>
  14. <サブカウント={カウント} />
  15. </サスペンス>
  16. <div>カウント {カウント}</div>
  17. </>
  18. );
  19. };

で:

  • 更新は毎秒トリガーされ、ステータスカウントが count => count + 1 に更新されます。
  • 非同期リクエストはSubで開始されます。リクエストが返される前に、サスペンスをラップするSubがフォールバックをレンダリングします。

リクエストが 3 秒後に返されると仮定すると、理想的には、リクエストが開始される前と開始された後の UI は次のように表示されます。

  1. // Subでリクエストが開始される前に
  2. <div class="sub">私はsubです、数えてください  0です</div>
  3. <div>カウント  0です</div>
  4.  
  5. // 最初の1秒でサブリクエストが開始されました
  6. <div>読み込み中...</div>
  7. <div>カウント  1 </div>
  8.  
  9. // 2 秒目にサブリクエストが開始されました
  10. <div>読み込み中...</div>
  11. <div>カウント  2です</div>
  12.  
  13. // 3秒後にサブリクエストが開始されました
  14. <div>読み込み中...</div>
  15. <div>カウント  3 </div>
  16.  
  17. // Subのリクエストが成功した後
  18. <div class="sub">私はサブです、リクエスト成功、カウント  4です</div>
  19. <div>カウント  4です</div>

ユーザーの観点から見ると、同時に実行されるタスクが 2 つあります。

  1. Subのタスクをリクエストする(最初のdivの変更を観察する)
  2. countのタスクを変更する(2番目のdivの変更を観察)

サスペンスは「マルチタスク同時実行」の直感的な感覚をもたらします。

そのため、非同期モードも並行モードに名前が変更されました。

解決不可能なバグ

では、アップデートに対応するサスペンスの優先度は高いのでしょうか、低いのでしょうか?

リクエストが成功した場合、合理的なロジックは「成功した UI をできるだけ早く表示する」ことです。したがって、サスペンスに対応するアップデートは優先度の高いアップデートにする必要があります。したがって、この例では 2 種類の更新があります。

サスペンスに対応する高優先度IO更新(u0と呼ばれる)

1 秒あたりに生成される低優先度 CPU 更新の数 (u1、u2、u3 などと呼ばれます)。

有効期限アルゴリズムの場合:

  1. // u0 の優先度は u1、u2、u3 よりもはるかに高いです...
  2. u0.有効期限 >> u1.有効期限 > u2.有効期限 > …

u0 の優先度が最も高いため、u1 以降の更新は u0 が完了するまで待機する必要があります。

そして、u0 は、実行する前に「要求が完了する」まで待機する必要があります。したがって、リクエストが開始される前と開始された後に UI は次のように表示されます。

  1. // Subでリクエストが開始される前に
  2. <div class="sub">私はsubです、数えてください  0です</div>
  3. <div>カウント  0です</div>
  4.  
  5. // 最初の1秒でサブリクエストが開始されました
  6. <div>読み込み中...</div>
  7. <div>カウント  0です</div>
  8.  
  9. // 2 秒目にサブリクエストが開始されました
  10. <div>読み込み中...</div>
  11. <div>カウント  0です</div>
  12.  
  13. // 3秒後にサブリクエストが開始されました
  14. <div>読み込み中...</div>
  15. <div>カウント  0です</div>
  16.  
  17. // Subのリクエストが成功した後
  18. <div class="sub">私はサブです、リクエスト成功、カウント  4です</div>
  19. <div>カウント  4です</div>

ユーザーの視点から見ると、2 番目の div は 3 秒間停止し、その後突然 4 秒に変わります。

したがって、CPU を集中的に使用するシナリオのみを考慮する場合、「優先度の高い更新を最初に実行する」アルゴリズムに問題はありません。

ただし、IO を集中的に使用するシナリオを考慮すると、優先度の高い IO 更新は優先度の低い CPU 更新をブロックすることになり、これは明らかに間違っています。

したがって、expirationTime アルゴリズムは同時更新を適切にサポートできません。

ExpirationTimeアルゴリズムオンラインデモ[1]

バグの原因

有効期限アルゴリズムの最大の問題は、有効期限フィールドが「優先度」と「バッチ」の概念を結合し、モデルの表現力を制限していることです。

その結果、優先度の高い IO 更新は、優先度の低い CPU 更新と同じ「バッチ」にグループ化されなくなります。その後、優先度の低い CPU 更新は、優先度の高い IO 更新が処理されるまで待機する必要があります。

実際の状況に応じて、異なるアップデートを柔軟に「バッチ」に分割できる場合、このバグは発生しません。

リファクタリングが差し迫っており、リファクタリングの目標は明確です。つまり、「優先度」と「バッチ」を 2 つのフィールドに分割することです。

車線スケジューリングアルゴリズム

新しいスケジューリング アルゴリズムは Lane と呼ばれます。「優先度」と「バッチ」はどのように定義されますか?

優先度については、レーン は 32 ビットの整数であり、最上位ビットが符号ビットであるため、最大 31 ビットが計算に参加できます。

異なる優先度は異なるレーンに対応し、ビットが低いほど優先度が高くなります。例:

  1. // SyncLaneに対応し、最も優先度が高い
  2. 0b00000000000000000000000000000001
  3. // InputContinuousLane に対応
  4. 0b00000000000000000000000000000100
  5. // DefaultLane に対応
  6. 0b0000000000000000000000000010000
  7. // IdleLane に対応
  8. 0b0100000000000000000000000000000
  9. // OffscreenLaneに対応し、最も優先度が低い
  10. 0b100000000000000000000000000000

「バッチ」はレーンによって定義されます。レーンも 32 ビットの整数で、「1 つ以上のレーンの集合」を表します。

ビット演算を使用すると、複数のレーンを同じバッチに簡単にグループ化できます。

  1. //使用するバッチ
  2. lanesForBatch = 0 とします。
  3.  
  4. 定数レーンA = 0b0000000000000000000000001000000;
  5. 定数レーンB = 0b0000000000000000000000000000000001;
  6.  
  7. // バッチにlaneAを含める
  8. レーンAのバッチ処理
  9. // バッチにlaneBを含める
  10. レーンバッチ |= レーンB;

上記の Suspense バグは、expirationTime アルゴリズムがバッチを柔軟に定義できないために発生します。

レーンには、このような懸念はまったくありません。同じ「バッチ」として定義する優先度 (レーン) は、ビット操作を使用して簡単に処理できます。

レーンアルゴリズムオンラインデモ[2]

要約する

スケジューリング アルゴリズムでは、次の 2 つの問題を解決する必要があります。

  1. 優先順位を選択
  2. バッチを選択

有効期限アルゴリズムで使用される有効期限フィールドは、これら 2 つの概念を結合しているため、柔軟性に欠けます。

レーンアルゴリズムの出現により、上記の問題は解決されます。

参考文献

[1] ExpirationTimeアルゴリズムのオンラインデモ:

https://codesandbox.io/s/usetransition-stop-reacting-passed-props-updates-forked-5e7lh

[2] レーンアルゴリズムオンラインデモ:

https://codesandbox.io/s/usetransition-stop-reacting-passed-props-updates-zoqm2?file=/src/index.js

<<:  データの筒状のビジョンを避け、人間と機械の調和のとれた共生関係を築く

>>:  認知知能は魔法のようなもの:2021 年の主要なブレークスルーを振り返る

ブログ    
ブログ    
ブログ    
ブログ    

推薦する

360 が顔認識分野に参入。「セキュリティ」の壁をどう克服するか?

スマートフォンや駅で顔認識技術が大規模に導入され始めており、誰もがこの新しい技術に精通しているはずで...

...

...

...

マイクロソフトCEOナデラ氏がグーグルを批判、AIに対する大きな懸念を表明

マイクロソフトはAIを理論から現実のものにしてきたリーダーであり、2019年のブログ投稿で多かれ少な...

視覚化と人工知能の強力な組み合わせ!

視覚化と視覚分析では、高帯域幅の視覚認識チャネルを使用してデータをグラフィック表現に変換し、インタラ...

グラフニューラルネットワークが深くなるほど、パフォーマンスは向上しますか?

数十または数百の層を持つニューラル ネットワークの応用は、ディープラーニングの重要な機能の 1 つで...

バックトラッキングアルゴリズム: 組み合わせ問題を解決しましょう!

[[379493]]バックトラッキングアルゴリズムをほとんど忘れてしまいましたか?組み合わせ問題を...

電力業界における人工知能開発の現状

今日は、人類が初めて電気を家庭や企業に供給するようになってから 140 年目の記念日です。電力産業は...

軍事用AIは普及するだろうか?公共の安全を重視すべきか、住民のプライバシーを重視すべきか?

[[227907]]ここ数カ月、軍事用AIと能動攻撃兵器の問題が話題になっており、多くのAI研究者...

もう学べないの? MIT CSおよびEEオンラインコースが利用可能になりました

[[320783]]流行病のため、MIT学長は3月初旬に残りの授業をすべてオンラインに移行するという...

GPU + 生成AIが時空間データ分析の改善に貢献

翻訳者|朱 仙中レビュー | Chonglou導入携帯電話、気候センサー、金融市場取引、車両や輸送コ...

機械学習を攻撃に利用する9つの方法

機械学習と人工知能 (AI) は、一部の脅威検出および対応ツールの中核技術になりつつあります。サイバ...

人工知能とデジタル技術はどのようにエネルギー効率を向上させるのでしょうか?

世界的なエネルギー危機が深刻化するにつれ、エネルギーの使用と管理の技術の継続的な開発と進歩も促進され...

自動化された機械学習: よく使われる 5 つの AutoML フレームワークの紹介

AutoML フレームワークによって実行されるタスクは、次のように要約できます。データを前処理して...