設問とコンテキスト
この質問は、ブラウザの並行性、共有メモリ、およびライフサイクル制御を評価するものです。Atomics.waitAsync()は、共有された整数位置が期待通りの値を保持している間にPromiseを返すため、呼び出し元はUIの処理を継続できます。これは、SharedArrayBufferを基盤とするInt32ArrayまたはBigInt64Arrayのビューのみを受け入れます。優れた回答では、待機プロトコルを所有権、キャンセル処理、および実用的なフォールバックと結びつけて説明します。
面接官が評価するポイント
- メインスレッドにおけるノンブロッキングな待機と、Worker内でのブロックを伴う可能性がある
Atomics.wait()を明確に区別できているか。 - 状態値、通知タイミング、タイムアウト、および重複通知に対して明示的な不変条件(インバリアント)が定義されているか。
- キャンセル、ページの非表示化、Workerのクラッシュ、およびバッファの解放が安全に行われているか。
- クロスオリジン分離(cross-origin isolation)、機能検知、およびTransferableによるフォールバックを理解しているか。
明確化のための質問
データのドロップが許容されるか、レイテンシの目標値、プロデューサーとコンシューマーの数、およびWorker間でメモリを共有する必要があるかを確認します。ブラウザのサポートマトリクス、ページがセキュアコンテキストやクロスオリジン分離を使用できるかについて質問します。サードパーティ製スクリプト、iframe、またはログインポップアップがCOOP/COEPの影響を受けるかを確認します。キャンセルがユーザーによる停止、タイムアウト、またはページのティアダウンのいずれを意味するのかを定義します。
30秒での回答アウトライン
共有領域を制御ワードとデータスロットに分割し、0=waiting、1=ready、2=cancelled、3=closedなどのバージョン管理された状態を使用します。コンシューマーはアトミックに状態を読み取り、待機中である場合にのみAtomics.waitAsyncを呼び出します。プロデューサーはデータを書き込み、Atomics.storeで状態を公開してAtomics.notifyを呼び出します。Promiseは単なる起床(wake-up)結果にすぎないため、コンシューマーは処理前に状態を再確認する必要があります。タイムアウトとキャンセルは状態遷移として扱い、バッファはすべてのWorkerが終了した後にのみ解放します。非対応ブラウザでは、同一のキャンセルおよびエラーセマンティクスを持つTransferableチャンクを使用します。
ステップごとの解決策
1. 共有状態と所有権の定義
制御領域には、プロトコルバージョン、状態、シーケンス番号、データ長、およびクローズフラグを含める必要があります。プロデューサーは自身が所有するスロットにのみ書き込みを行い、書き込み完了後に準備完了状態を公開します。コンシューマーは処理を行う前に状態とシーケンスを読み取ります。1回の通知を1つの永続的なメッセージとして扱うことはできません。通知は合流、競合、または以前のバッチを参照する可能性があるため、起床するたびに状態を再確認します。
2. waitAsyncとnotifyの正しい使用
Atomics.waitAsync(view, index, expected, timeout)は、まず指定された位置の値を比較します。値が異なる場合はnot-equalを返し、タイムアウト時はtimed-outを返します。それ以外の場合はawait可能なPromiseを返します。プロデューサーは状態を変更した後、Atomics.notify(view, index, count)を呼び出します。このプロトコルは次のように表現できます:
const control = new Int32Array(new SharedArrayBuffer(16));
const WAITING = 0;
const READY = 1;
const CANCELLED = 2;
async function waitForData(timeout = 1000) {
const result = Atomics.waitAsync(control, 0, WAITING, timeout);
const outcome = result.async ? await result.value : result.value;
const state = Atomics.load(control, 0);
return { outcome, state };
}
function publishData() {
Atomics.store(control, 0, READY);
Atomics.notify(control, 0, 1);
}3. キャンセル、タイムアウト、クローズ状態の追加
既存のPromiseを無理に中断しようとしないでください。代わりにキャンセル状態を書き込み、待機中のスレッドに通知します。待機側はok、timed-out、またはnot-equalを受け取った後、状態を再読み込みして、継続、再試行、または終了を選択します。クローズ時は、まず生成を停止し、領域をクローズ済みとしてマークしてからすべての待機側に通知します。Workerの終了が確認された後にのみ、バッファを解放する必要があります。
4. 競合状態とバックプレッシャーの処理
複数のプロデューサーが存在する場合、スロットを確保するためにCASまたはアロケータが必要となり、同じシーケンスに対して同時に書き込んではいけません。キューが満杯になった場合は、プロダクトの仕様要件に従ってドロップ、上書き、またはバックプレッシャーを選択し、キューの深さとドロップ数を記録します。コンシューマーは1回の起床を1つのメッセージとして解釈するのではなく、ループ内で公開済みのすべてのシーケンス番号を処理し尽くす(drainする)必要があります。
5. クラッシュとページライフサイクルの管理
メインスレッドがWorkerの状態マシンとハートビートを管理します。errorまたはデッドライン到達時には、新規書き込みを停止し、タスクを失敗としてマークして、残りのWorkerに終了を要求します。ページの非表示化やコンポーネントのアンマウント時には、キャンセルを送信し、一定時間待機した後にterminateします。再起動時は、古いシーケンスや状態値を再利用するのではなく、新しい制御領域バージョンを作成します。
6. 機能検知と安全なフォールバック
起動時にcrossOriginIsolated、SharedArrayBuffer、Atomics.waitAsync、およびWorkerのサポート状況を確認します。クロスオリジン分離はサードパーティ製リソースやポップアップの挙動を変化させるため、パフォーマンスを理由にセキュリティポリシーを緩和すべきではありません。必要な機能が利用できない場合は、キャンセル、タイムアウト、進捗、およびエラーのフィールドを維持したまま、TransferableなArrayBufferチャンクまたは通常のpostMessageを使用します。パスの利用割合とテールレイテンシを計測します。
質の高い模範解答
バージョン管理された制御領域を作成する前に、機能検知を行い、セキュアコンテキストおよびクロスオリジン分離の要件を確認します。プロデューサーはデータを書き込んだ後にアトミックに状態を公開し、待機側に通知します。コンシューマーは状態が待機中である場合にのみAtomics.waitAsyncを呼び出し、Promiseの結果に関わらず状態とシーケンスを再読み込みします。キャンセル、タイムアウト、およびクローズは明示的な状態として扱います。シャットダウン時は生成を停止し、待機側を起こし、Workerの終了を確認した後に初めて共有メモリを解放します。所有権とシーケンス番号によって二重消費を防ぎ、キュー満杯時の挙動は計測に基づいて設計されたプロダクト判断とします。共有メモリや該当APIが利用できない場合は、Transferableチャンクが同じライフサイクルとエラーの契約を提供します。
よくある間違い
notifyがメッセージごとに厳密に1回の起床を保証すると誤認すること。- メインスレッドでブロッキングなwaitを呼び出し、UIをフリーズさせること。
- 共有状態やシーケンスを再確認せずに、Promiseの結果を盲信すること。
- Workerがまだ実行中であるにもかかわらず、キャンセル時に即座にバッファを解放してしまうこと。
- クロスオリジン分離やサードパーティリソースの制約を無視すること。
postMessageに切り替えた際に、タイムアウト、キャンセル、またはエラーのセマンティクスを失ってしまうこと。
フォローアップ質問
どのような場合にwaitではなくwaitAsyncを選択しますか?
waitAsyncは呼び出し元スレッドをブロックせずにPromiseを返すため、メインスレッドやイベント駆動型コードに適しています。waitはWorkerをブロックできますが、そのWorkerのブロック時間がUIや他の重要な処理に悪影響を与えない場合に限られます。どちらも状態チェック、タイムアウト、およびクローズプロトコルが必要です。
なぜ通知後に状態を再読み込みする必要があるのですか?
通知は条件が変更された可能性があることを伝えているにすぎません。通知が合流したり、競合する別のスレッドを起こしたり、以前のバッチに対応している場合があります。状態、シーケンス、およびデータ長を読み取ることで、データが実際に利用可能かどうかを確定します。
キャンセル後の不要な処理をどのように防ぎますか?
共有状態にキャンセルを公開し、スロットの確保時、処理前、および結果の確定前にそれを確認します。結果メッセージにタスクバージョンを含め、メインスレッドが無効化されたバージョンの結果を拒否できるようにします。
どのような場合に共有メモリの使用を断念すべきですか?
分離ヘッダーによって重要な依存関係が壊れる場合、ブラウザの対応範囲が不十分な場合、デバッグコストが得られるメリットを上回る場合、またはスループット要件が控えめな場合は、Transferableチャンクを使用します。共有メモリが常に最適な手段であると思い込まず、機能サポート状況とテレメトリに基づいて適切なパスを選択します。