フレームワーク作者の視点から: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 年の主要なブレークスルーを振り返る

ブログ    

推薦する

分析と AI で注意すべき 7 つの致命的な間違い

2017年、『エコノミスト』誌は、データが石油を上回り、世界で最も価値のある資源になったと宣言しまし...

...

マイクロソフトは、AIチップが十分に入手できない場合、データセンターのサービスが中断される可能性があると警告している

CNBCによると、7月29日、マイクロソフトは最近発表した財務報告書の中で、データセンターのサービス...

...

DeepMindの強化学習法はAIと人間のより良いコラボレーションを約束する

[[437442]] [51CTO.com クイック翻訳]囲碁からスタークラフト、Dotaまで、多く...

...

視覚畳み込みニューラルネットワークモデルを習得し、画像認識技術の分野を探索します。

ディープラーニングに取り組む過程で、著者が最も興味を持ったのは、オブジェクトを分類するためのいくつか...

AI モデルの「アウトソーシング」をやめましょう!新しい研究によると、機械学習モデルのセキュリティを弱める「バックドア」の一部は検出できないことが判明した。

悪意のある「バックドア」が埋め込まれたモデルが、何百万、何十億ものパラメータを持つモデルの中に、何者...

Meta が AI の公平性を評価するための FACET データセットをリリース

Meta は 9 月 4 日に、研究者がコンピューター ビジョン モデルのバイアスを確認するのに役立...

GPT-3を超えて、DeepMindは新しいお気に入りのGatoをリリースしましたが、「スープは変えても薬は変えない」と疑問視されています

大規模な言語モデリングにヒントを得て、Deepmind は同様のアプローチを適用し、マルチモーダル、...

...

AIは人間に取って代わるでしょうか?シリコンバレーの大物が人工知能の将来の発展の傾向を解説

[[378409]]人工知能は間違いなく将来のトレンドであり、AIは将来の経済の発展を推進するでしょ...

AI が「もや」を取り除くのに役立ちます: うつ病の治療における機械学習の応用

この記事は、公開アカウント「Reading the Core」(ID: AI_Discovery)か...

...

...