無鎖設計指導建議 (Lock-Free Design Guidelines)

Overview Table

指導建議 核心內容
(1) 原型先用 seq_cst 先以 memory_order_seq_cst 寫出正確的實作,通過測試後才把放寬內存序當作優化
(2) 規劃記憶體回收方案 執行緒還在訪問的節點不能刪;三大技術:待刪鏈計數、hazard pointer、引用計數
(3) 小心 ABA 問題 CAS 只比較不比較歷史;值 A→B→A 會讓過期的 CAS 誤判成功 → 加 ABA 計數器
(4) 識別忙等待、幫助其他執行緒 忙等待迴圈 = 變相的鎖;把私有步驟改成原子步驟,讓等待者代為完成 (helping) 恢復無鎖屬性

第 6 章的併發設計通用準則(見 06-Lock-Based-Data-Structures/01-Concurrent-Design-Guidelines)依然適用;以下是從本章例子萃取、專屬無鎖結構的四條建議。

建議一:原型設計先用 std::memory_order_seq_cst

seq_cst 讓所有操作構成一個全域總序,推理最簡單。本章所有例子都是先用 seq_cst 寫到正確,才逐步放寬。

順序:先對,再快

「seq_cst 版本正常運作」是放寬的前置條件,不是可以跳過的步驟。

建議二:對無鎖記憶體回收制定方案

無鎖程式碼與基於鎖的程式碼最大的差異就是記憶體管理:只要還有執行緒在訪問節點,就不能 delete;但為了控制記憶體用量,又希望越早刪越好。本章三種技術(詳見 07-Lock-Free-Data-Structures/03-Memory-Reclamation-Strategies):

共同思想:追蹤有多少執行緒正在訪問指定物件。其他替代方案:

建議三:小心 ABA 問題

定義:基於 CAS 的演算法中,CAS 只比較「值是否相同」,無法察覺「值曾經變過又變回來」。書中流程:

  1. 執行緒 1 讀原子變數 x,值為 A
  2. 執行緒 1 基於 A 做了些事(解引用指標、查詢等)
  3. 作業系統把執行緒 1 掛起
  4. 其他執行緒把 x 改成 B
  5. 又有執行緒修改了 A 相關的資料,使執行緒 1 手上關於 A 的資訊失效(如釋放了 A 指向的記憶體,或改了其中的值)
  6. 再有執行緒把 x回 A——若 A 是指標,可能是新物件恰好重用了舊位址
  7. 執行緒 1 恢復,對 x 做 CAS 比對 A:成功,但這是「錯誤的 A」——第 2 步讀到的資料早已失效,後續操作損毀資料結構

以「重用節點的無鎖堆疊 pop」為例:

堆疊: head → A → B → C            執行緒 1 想 pop A

執行緒 1                           其他執行緒
--------                           ----------
old_head = A
next = A->next(= B)
   ── 被 OS 掛起 ──
                                   pop A、pop B(A 被回收進 free list)
                                   push D、push「新節點」→ 重用 A 的位址
                                   堆疊: head → A' → D → C
恢復執行
CAS(head: A → B) 成功!             ← 位址仍是 A,比較通過
                                     但 A' 的 next 其實是 D
結果: head → B(已刪除的節點!)      → 懸空指標、D 遺失、結構損毀

解法:ABA 計數器(世代計數)——讓 x 附帶一個計數器,CAS 對「值 + 計數」的整體操作,每次修改計數遞增。即使值變回 A,計數已不同,過期的 CAS 必定失敗。

struct counted_ptr {         // CAS 的單位是整個結構體
    node*    ptr;
    unsigned aba_count;      // 每次成功修改 +1
};
std::atomic<counted_ptr> x;  // 需能放進平台原子指令(如 DWCAS)
「CAS 成功」不等於「期間沒人動過」

CAS 語義是「值相等就交換」,不是「無人修改過才交換」。凡是把 CAS 當「無人動過」用的推理,都要檢查 ABA。

建議四:識別忙等待迴圈,並幫助其他執行緒


Exam/Test Patterns

情境/關鍵字 答案
「無鎖原型該用哪種內存序起手?」 memory_order_seq_cst;正確後才放寬,放寬是優化
「放寬後測試全過,就代表內存序正確?」 ——交錯組合無法窮舉,需演算法檢查器才能保證
「無鎖 vs 有鎖程式碼最大差異?」 記憶體管理:有執行緒訪問時不能刪節點
「三種安全回收技術?」 待刪鏈 + 執行緒計數、hazard pointer、引用計數(皆為追蹤訪問執行緒數)
「循環使用節點的代價?」 避開非法記憶體,但引入 ABA 問題
「x 從 A→B→A,CAS 成功卻壞掉」 ABA 問題——比較通過但拿到「錯誤的 A」
「ABA 的一般解法?」 變數附帶 ABA 計數器,CAS 對「值+計數」整體操作,每次修改遞增
「哪類演算法最容易 ABA?」 free list / 節點重用的演算法;直接還給分配器則不會
「本章的堆疊/佇列有 ABA 問題嗎?」 沒有——hazard pointer / 引用計數保證使用中的節點不被刪除重用
「等別的執行緒完成才能前進的迴圈」 忙等待 = 變相的鎖;用 helping 讓等待者代做剩餘步驟
「helping 的前提改造?」 把非原子的私有步驟改為原子變數 + CAS,任何執行緒都能完成它