具代表性的面試主題

如何用 C++26 hazard pointer 安全回收無鎖鏈結串列節點?

程式題困難
Offer.cc 編輯團隊發佈 更新

題幹

請實作多執行緒無鎖堆疊的 pop 路徑,要求讀執行緒不會存取已釋放節點。說明 hazard pointer 保護窗口、退休清單掃描、ABA 邊界、記憶體序與測試方法。

題目與背景

你有高併發無鎖堆疊,節點以原子指標連結。執行緒從頭部讀取並 CAS 摘除,但另一執行緒可能同時釋放節點。請用 C++26 hazard_pointer 思路設計安全回收,允許多個讀者並行且不能以全域大鎖包住堆疊。

面試官考察什麼

重點是 hazard pointer 保護「正在讀取的地址」,不是讓節點永久存活。讀者先發布 hazard,再確認原子 head 仍指向該節點;摘除成功後放入退休清單,掃描所有 hazard 後才回收。也要說明 acquire/release、註冊退出、掃描成本,以及 ABA 需獨立處理。

先問清楚的澄清問題

資料結構與進度保證

確認是 Treiber 堆疊、鏈結串列或雜湊桶,要求 lock-free 或 wait-free,以及是否允許執行緒本地退休清單。

執行緒生命週期

確認如何取得 hazard slot,執行緒退出時如何撤銷保護並移交退休節點。崩潰執行緒不能留下永遠不可回收的記錄。

ABA 與標記策略

確認地址能否重用、是否有版本計數或 tagged pointer。Hazard pointer 防止受保護節點釋放,但不自動阻止 ABA。

30 秒回答框架

「讀者先原子讀取 head,把地址發布到 hazard slot,再重新讀取 head;兩次一致才允許解引用。CAS 摘除成功後節點進入退休清單,不立即 delete。定期收集所有 hazard 地址,只回收不在集合中的退休節點。執行緒退出前清除 hazard 並交接退休清單。ABA 要用版本計數或其他方案另外處理。」

深入解答步驟

第一步:定義 hazard slot 與退休清單

每個可能解引用共享節點的執行緒擁有 hazard slot。退休清單保存已摘除但尚未安全回收的節點,slot 生命週期和註冊必須可追蹤。

第二步:建立發布與確認窗口

讀 head 後以 release 或等價順序寫入 hazard,再以 acquire 重新讀 head。只有兩次一致才可讀節點欄位;變化時清除 slot 並重試。這可阻止節點在發布前被回收。

第三步:執行 CAS 並延後回收

讀取 next 後用 compare_exchange 摘除 head。CAS 失敗時清除 hazard 並重試;成功後把舊節點放入退休清單,完成使用後才清除 slot。任何路徑都不能直接 delete 共享節點。

第四步:掃描並回收

掃描所有執行緒 hazard slot,建立受保護地址集合。遍歷退休清單,只回收不在集合中的節點。掃描閾值可按 slot 數量和清單長度調整。

第五步:處理 ABA 與記憶體序

延後回收可降低地址重用,但不等於消除 ABA。若堆疊快速摘除與重新入棧,使用版本計數或 tagged pointer。head、hazard 和節點欄位要有清楚 happens-before 關係。

第六步:處理執行緒退出與例外

執行緒停止讀取前清除 hazard,退休節點交給存活回收者或全域域。註冊表需能偵測 owner 狀態;析構只能在確認沒有讀者後執行。

第七步:測試安全性與效能

用 ThreadSanitizer、隨機排程與壓力測試覆蓋 CAS 失敗、掃描並行、退出、重用和例外路徑。加入延遲釋放哨兵驗證沒有 use-after-free,並量測掃描耗時、清單峰值、吞吐與尾延遲。

高品質示例回答

我會為每個讀者分配 hazard slot。pop 先讀 head、發布 hazard、再次讀 head,只有一致才讀 next 並 CAS;失敗就清除並重試。摘除成功後節點進入退休清單,掃描全部 hazard 後只回收未受保護節點。退出要清除 slot 並移交清單,ABA 用版本計數或 tagged pointer 另行解決。測試高競爭、重用、退出和例外,檢查 use-after-free、掃描成本與尾延遲。

常見錯誤

  • 錯誤: 讀 head 後直接存取節點。→ 原因: 發布保護前節點可能已被回收。→ 改進: 發布 hazard 後重新確認 head。
  • 錯誤: CAS 成功後立即 delete。→ 原因: 其他讀者仍可能持有保護窗口。→ 改進: 先退休,掃描後回收。
  • 錯誤: 認為 hazard pointer 自動解決 ABA。→ 原因: 延後回收不保證邏輯版本不變。→ 改進: 使用版本計數或 tagged pointer。
  • 錯誤: 只用 relaxed 原子。→ 原因: 可能缺少發布與確認的可見性。→ 改進: 明確 acquire/release 與 happens-before。

追問與回答

追問 1:為什麼要重新讀 head?

第一次讀取到發布 hazard 之間存在窗口,其他執行緒可能摘除並回收節點。重新確認保證發布時節點仍是 head,否則重試。

追問 2:掃描時新發布 hazard 會被漏掉嗎?

正確協定要求讀者先發布再確認,確認失敗就重試。只要所有讀者遵守協定,退休且不在保護集合的節點才可回收。

追問 3:退休清單會無限增長嗎?

讀者長時間持有 hazard、執行緒停止或掃描太少時可能增長。設定閾值、監控峰值並確保退出清理,必要時由回收執行緒主動掃描。

追問 4:hazard pointer 與 epoch-based reclamation 如何選?

Hazard pointer 精確保護少量地址,適合動態讀路徑,但掃描 slot 成本較高。Epoch 批量回收效率高,卻可能被停頓執行緒拖住。依讀者數量、停頓容忍度和記憶體上限選擇。

公開來源

同類題目

相關面試工具

用 Screenshot 處理演算法題

截圖題目後,依序看約束、解法、程式碼、邊界條件和複雜度。

查看工具