題幹與適用場景
題目考察瀏覽器並行、共享記憶體與生命週期治理。Atomics.waitAsync() 在共享整數位置符合條件前回傳 Promise,呼叫端可繼續處理 UI;它只能用於基於 SharedArrayBuffer 的 Int32Array 或 BigInt64Array 檢視。回答要把等待協定、資料所有權、取消語意與降級路徑串成完整方案。
面試官考察什麼
- 能否區分主執行緒可用的非阻塞等待與 Worker 中可能阻塞的
Atomics.wait()。 - 能否定義狀態值、通知時機、逾時與重複通知下仍成立的不變量。
- 能否處理取消、頁面隱藏、Worker 崩潰與共享緩衝區釋放。
- 能否說明跨來源隔離、瀏覽器能力檢測與 Transferable 降級的影響。
回答前需要釐清的問題
先確認資料是否允許丟棄、目標延遲、生產者與消費者數量、是否需要跨 Worker 共享,以及瀏覽器支援矩陣。也要確認頁面能否啟用安全情境和 cross-origin isolation,第三方腳本、iframe 或登入彈窗是否受 COOP/COEP 影響。最後確定取消是使用者主動停止、逾時,或頁面生命週期結束。
30 秒回答框架
我會把共享區分成控制字與資料區,控制字使用 0=等待、1=就緒、2=取消、3=關閉 等版本化狀態。消費者先原子讀取狀態,若仍為等待就呼叫 Atomics.waitAsync;生產者寫入資料後用 Atomics.store 發布狀態並 Atomics.notify。Promise 只表示等待結果,真正處理前仍需重新讀取狀態。逾時與取消透過狀態轉換實作,所有 Worker 結束後才釋放緩衝區;不支援共享記憶體時改用 Transferable 分塊訊息,維持相同取消與錯誤語意。
分步驟深入解答
1. 設計共享狀態與所有權
控制區至少包含協定版本、狀態、序號、長度與關閉標記。生產者只寫入自己擁有的槽位,完成寫入後才發布就緒狀態;消費者讀取狀態與序號後處理資料,不能依賴一次通知來保存訊息。每次喚醒都要重新檢查狀態,因為通知可能合併、提前到達或對應上一批資料。
2. 正確使用 waitAsync 與 notify
Atomics.waitAsync(view, index, expected, timeout) 會先比較目前位置;值不等於 expected 時立即回傳 not-equal,逾時回傳 timed-out,否則回傳可等待的 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 確認退出,避免釋放仍被存取的 SharedArrayBuffer。
4. 處理並行競態與背壓
多個生產者需要用 CAS 或分配器領取槽位,不能讓它們同時寫同一序號。佇列已滿時依業務選擇丟棄、覆蓋或背壓,並記錄佇列深度與丟棄數。消費者不能把一次喚醒當成一則訊息,應循環讀取所有已發布序號,直到佇列為空或收到取消狀態。
5. 管理 Worker 崩潰與頁面生命週期
主執行緒維護 Worker 狀態機與心跳;發生 error 或逾時就停止新寫入,標記任務失敗並讓剩餘 Worker 退出。頁面進入背景或元件卸載時送出取消通知,等待有限時間後終止 Worker。重新啟動時建立新版本的控制區,避免重用含有舊序號與舊狀態的記憶體。
6. 做能力檢測與安全降級
啟動時檢查 crossOriginIsolated、SharedArrayBuffer、Atomics.waitAsync 與 Worker 能力。跨來源隔離會影響第三方資源與彈窗,不能為了效能繞過安全策略。不符合條件時使用 Transferable ArrayBuffer 分塊或一般 postMessage,並保留相同取消、逾時、進度與錯誤欄位;監控各路徑占比與尾延遲。
高品質示範回答
我會先做能力檢測並確認頁面符合安全情境與跨來源隔離要求,再建立版本化控制區。生產者寫完資料後用原子儲存發布狀態並通知,消費者在狀態仍為等待時呼叫 Atomics.waitAsync,無論 Promise 以何種結果結束,都重新讀取狀態與序號。取消、逾時與關閉都是明確狀態,關閉順序是停止生產、通知等待者、確認 Worker 結束、最後釋放共享記憶體。佇列用所有權與序號避免重複消費,滿載策略依業務選擇背壓或丟棄並記錄指標。若共享記憶體或 API 不可用,就切換到 Transferable 分塊訊息,維持相同生命週期與錯誤協定。
常見錯誤
- 認為
notify保證一則訊息對應一次喚醒。 - 在主執行緒呼叫會阻塞的等待方式,造成頁面卡頓。
- 只等待 Promise 結果,不重新讀取共享狀態與序號。
- 取消時直接釋放緩衝區,留下仍在執行的 Worker。
- 忽略
SharedArrayBuffer的跨來源隔離與第三方資源限制。 - 降級到
postMessage後遺失逾時、取消或錯誤語意。
追問及應對
waitAsync 和 wait 如何選擇?
waitAsync 回傳 Promise,不阻塞呼叫執行緒,適合主執行緒或需要繼續處理事件的程式;wait 可阻塞 Worker,但必須保證阻塞不會影響 UI 或其他關鍵任務。兩者都要配合狀態檢查、逾時與關閉協定。
為什麼通知後還要重新讀取狀態?
通知只提示某個等待條件可能變化,可能出現合併通知、競爭喚醒或狀態已被其他消費者改變。重新讀取狀態、序號與長度才能決定是否有可處理資料。
如何避免取消後又處理一批資料?
把取消寫入共享狀態,並在取槽、處理前與提交結果前各檢查一次;結果提交也帶任務版本。主執行緒撤銷版本後,舊 Worker 的結果必須丟棄。
何時應放棄共享記憶體?
當隔離標頭會破壞關鍵依賴、瀏覽器覆蓋不足、除錯成本超過收益或資料吞吐不高時,應使用 Transferable 分塊。透過能力與指標選擇路徑,不把共享記憶體當作預設最佳化。