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

ブログ    

推薦する

[GPT-4理論] GPT-4のコア技術を探る

序文GPT-4はリリースされてからしばらく経ちますが、セキュリティなどのさまざまな理由から、Open...

IoTとAIを活用した依存症治療

IoT および AI ベースのデバイスは、私たちの中毒的な習慣をきめ細かなレベルで監視できるため、ユ...

...

CV モデルが動作しないのはなぜですか?理由がこんなに単純だとは思いませんでした...

コンピューター ビジョン モデルのパフォーマンスが低下する理由は、アーキテクチャ設計上の欠陥、代表的...

機械学習は将来どこに向かうのでしょうか?インテル・南京大学共同研究センターが答えを提供する

[51CTO.com からのオリジナル記事] 人工知能アルゴリズムに関しては、ディープラーニングが現...

AIが3Aの傑作をプレイ、OpenAIは調査されるか? 2023年のAIパノラマレポートが10の予測を発表

State of Report は今年で 6 年目を迎え、人工知能分野の風向計となっています。業界お...

機械学習プロジェクトの 87% が失敗する 10 の理由

機械学習は、最近ニュースでよく耳にする言葉ですが、さらに多くのことを実現する可能性を秘めた技術です。...

...

ウェブデザインに人工知能を活用する10の方法

[[210916]]今日、Web デザインは絶えず進化しており、トップクラスの Web デザイナーは...

教師なし機械学習技術は金融セキュリティの懸念を解決できる

「テクノロジーがなければ、金融は存在しない。」モバイルインターネット時代の到来により、テクノロジーや...

ソフトウェアと自動化機器が持続可能性と回復力を向上させる方法

近年、需要の増加、エネルギーコストの高騰、持続可能性の問題が続く中、データセンターが注目を集めていま...

...

Google Brain Quoc、操作プリミティブから効率的なTransformerバリアントを検索するためのPrimerをリリース

[[426884]]モデルのパフォーマンスを向上させるには、パラメータを調整し、活性化関数を変更する...

ガートナー:金融業界はAIがもたらす変化を追求

私たちがよく話題にする金融テクノロジーとは、人工知能とデータを活用して新しいビジネス手法を見つけるも...