フロントエンドとrace condition

pjfb·2023년 2월 8일

レースコンディション

https://ja.wikipedia.org/wiki/%E7%AB%B6%E5%90%88%E7%8A%B6%E6%85%8B

簡単に言えば、2つ以上のスレッドが1つの共有リソースにアクセスして起こる状態(またはそれによって発生するバグ)と考えてください。

レースコンディションの良い例:最近、いくつかのクエリを高速で押しました。 その結果、検索語と画面の結果が一致しないバグが表示されます。

ここで2つ以上のスレッドとは何ですか、1つの共有リソースは何でしたか?

  • 複数のスレッドは検索要求です
    検索を実行すると fetch('/search?q=コーヒー') のようなコードが実行され、もしユーザが 2 つの検索を実行すると 2 つの fetch が実行されます。

  • 1つの共有リソースは検索結果View(UI)です
    fetchの後に実行されるコードは、検索結果を持ってUIを更新するコードが実行されることは明らかです。 すべての fetch は 1 つの検索結果リスト (UI) に書き込み操作を試みるので 共有リソース になります。

JavaScriptはシングルスレッドではありませんか?

YogiyoアプリはiOSネイティブ言語で書かれている可能性が高いですが、これらのRace ConditionのバグはWebで書かれたアプリでも十分に発生する可能性があります。

あなたのコードはシングルスレッドで実行されますが、ブラウザはマルチスレッドで動作するためです。

以下のコードは、Race Conditionのバグを生成する簡単な例です。

const SearchResult = () => {
   const [result, setResult] = useState();
  
   const onClickMusic = async (name: string) => {
     setResult(
       await fetch(`/music/${name}`),;
   };
  
   return(
     <div>
       <div>
         <button onClick={() => onClickMusic('music1')}>
           music 1
         </button>
         <button onClick={() => onClickMusic('music2')}>
           music 2
         </button>
         <button onClick={() => onClickMusic('music3')}>
           music 3
         </button>
       </div>
       <div>
         選択した曲の情報は{result}です。
       </div>
     </div>
   );
};

特別なコードではありません。 一般的に多く書かれたコードであり、このコードは上記のヨギヨバグをそのまま持っています。

どのような場合にバグが発生しますか?

music1をクリック -> fetch(music1) -> music2をクリック -> fetch(music2) -> setResult(music2) -> setResult(music1)

このような場合を考えてください。
__ユーザーが最後に押すと「music2」が表示されますが、画面には「music1」に関する情報が表示されます。

これらの問題の原因は、ネットワーク要求がどれくらいかかるかを知らず、遅く出発した要求が最初に到着する可能性があります。

この記事の最初の映像だけを見ても分かるように、これはまれに発生する現象ではなく、心を食べればとても簡単に再現できるという点にあります。

単にUIの不一致のバグですか?

そうではありません。
ネットワークが完了した後に実行されるコードがより複雑にチェーンされていて(複数の連鎖useEffectなど)、より多くの値を変更した場合、今は単純なUIのバグではなく本当のバグを作成できます。 難しくなる可能性があります。

どうすれば修正できますか?

Best Practice

Reactを使用している場合は、SuspenseとRender-as-you-fetchパターンに置き換えます。
私の他の記事から 言及したように、Suspenseは単にローディングSpinnerを回すためのパターンではありません。
詳細については、React 公式ドキュメントでの Suspense との競合状態 をご覧ください。

isLoading状態を活用する

最も簡単な方法です。
isLoadingという名前のステートをもう1つ作成し、すでに進行中のリクエストがある場合は、次のリクエストを無視します。

const Component=()=>{
   const [isLoading、setIsLoading] = useState(false;
   const [result, setResult] = useState();
  
   const onClickMusic = (name: string) => {
     if (isLoading) return;
    
     try{
       setIsLoading(true);
       setResult(
         await fetch(`/music/${name}`),
       );
     } finally {
       setIsLoading(false);
     }
   };
  
   /* .... */
};

この方法にはマイナーな欠点があります。

要求が終了するまで、コンポーネントはブロック状態になります。
ネットワーク要求は状態が悪い場合は10秒以上かかることもあり、インターネットがまったくならない場合は30~60秒が過ぎた後、結局何もしないまま__失敗状態に戻る場合もあります。

ネットワークの面白いことは、ロードに時間がかかりすぎてユーザーが別のアクションを取ると、そのアクションに対する要求は非常に迅速に戻る可能性があることです。
(ページが長時間表示されず、F5を押してすぐに表示される場合)
isLoadingメソッドは、これらの幸運な場合の可能性を根本的にブロックします。

(マイナーな欠点だと言っただけに必ず解決しなければならない問題ではないかもしれません。)

Fetch Counterの作成

より良いアプローチは、最後の要求のみを有効な要求として認識することです。 これはUXでより良い経験を提供します。

const Component=()=>{
   const fetchCounter = useRef(0;
   const [result, setResult] = useState();
  
   const onClickMusic = (name: string) => {
     const prevFetchCounter = ++fetchCounter.current;
     const newResult = await fetch(`/music/${name}`);
    
     // リクエスト間に異なるリクエストがあったかどうかを調べます。
     if(prevFetchCounter === fetchCounter.current) {
       setResult(newResult);
     }
   };
  
   /* .... */
};
  • fetchCounterという変数を作成し、要求の開始前にローカル変数にコピーします。
  • タスクを起動します。 (ここでの作業はfetchであり、通常は時間がかかります(0.1秒も時間がかかる作業です))。
    *作業が終了して戻って、地域変数最新の値(ref)を比較します。
  • 2つの値が同じ場合、途中で割り込んだ要求がないことを意味するため、結果を上書きします。
    *もし2つの値が異なる場合、fetchの途中で新しいfetchが実行されたことを意味します。 私が持っている newResultは今古い値なので、上書きせずに終了します。

数行以内の短いコードですが、簡単にすることができます。
このコードが不慣れな場合は、理解するまで読んでみてください。

仕上げ

레이스컨디션

0개의 댓글