無鎖概念與利弊權衡 (Lock-Free Concepts and Tradeoffs)
Overview Table
| 概念 | 重點 |
|---|---|
| 阻塞 vs 非阻塞 | 呼叫阻塞函式庫(mutex、條件變數、future)會掛起執行緒;不用阻塞函式庫的結構稱非阻塞 (non-blocking) |
| 自旋鎖 (spinlock) | 非阻塞但不是無鎖——仍有鎖,一次只允許一個執行緒 |
| obstruction-free(無阻礙) | 其他執行緒全部暫停時,任一執行緒能在有限時間完成操作 |
| lock-free(無鎖) | 多執行緒併發操作時,至少一個執行緒能在有限時間完成(系統整體有進展) |
| wait-free(無等待) | 每個執行緒都能在有限步驟內完成,不受其他執行緒影響 |
| 優點 | 最大化併發、強健性 (robustness)、無死結 (deadlock) |
| 缺點 | 複雜度高、原子操作昂貴、快取乒乓、可能出現活結 (livelock)、整體效能未必更好 |
阻塞 vs 非阻塞
使用互斥鎖 (mutex)、條件變數 (condition variable)、future 同步時,函式庫會呼叫阻塞操作掛起執行緒(作業系統將其時間片交給別人),直到解鎖、通知或 future 就緒才解除阻塞。不使用阻塞函式庫呼叫的資料結構與演算法稱為非阻塞結構。
- 阻塞的解除方式:互斥鎖解鎖、條件變數被通知、future 狀態就緒
- 非阻塞 ≠ 無鎖:非阻塞只是「不呼叫阻塞函式」,結構仍可能含鎖
第 5 章用 std::atomic_flag 實作的自旋鎖就是典型例子:
class spinlock_mutex {
std::atomic_flag flag;
public:
spinlock_mutex(): flag(ATOMIC_FLAG_INIT) {}
void lock() { whilememory_order_acquire); }
void unlock() { flag.clearmemory_order_release; }
};
lock() 沒有任何阻塞呼叫,只是在迴圈中自旋——所以它是非阻塞的;但它仍是一把鎖、一次只能有一個執行緒進入,所以不是無鎖結構。
常見混淆
「非阻塞 (non-blocking)」是最寬的類別;自旋鎖屬於「非阻塞—有鎖」。無鎖 (lock-free) 是非阻塞的子集,需要額外的進展保證。
非阻塞的三個層級
| 層級 | 定義 | 進展保證 | 備註 |
|---|---|---|---|
| obstruction-free(無阻礙) | 若其他執行緒都暫停,任一執行緒可在有限時間完成 | 最弱(孤立執行時才保證) | 實務少用;常用來描述一個失敗的 lock-free 實作 |
| lock-free(無鎖) | 多執行緒同時操作,經過有限時間其中一個執行緒必完成 | 系統整體有進展;個別執行緒可能飢餓 | 典型形式:compare_exchange 重試迴圈 |
| wait-free(無等待) | 每個執行緒都在有限步驟內完成,不管其他執行緒怎麼做 | 每執行緒都有進展上限 | 最難正確實作;不允許無上限的重試迴圈 |
包含關係(由寬到窄)
非阻塞 (non-blocking)
├── 非阻塞—有鎖(如:自旋鎖)
└── obstruction-free 無阻礙
└── lock-free 無鎖 ← 至少一個執行緒有進展
└── wait-free 無等待 ← 每個執行緒都有進展
- lock-free 的判別:使用「比較/交換」的程式碼,若其他執行緒掛起時 CAS 必能成功,即為無鎖;若失敗後需要自旋等別人,則只是「非阻塞—有鎖」
- lock-free 的重試迴圈可能讓某些執行緒飢餓(每次都在「錯誤的時間」執行而不斷重試)——wait-free 就是為了消除此問題
- 本章多數範例對
compare_exchange_weak/strong做次數無上限的迴圈,因此是 lock-free 但不是 wait-free
無鎖結構的利
- 最大化併發(活性):基於鎖的容器總有執行緒阻塞等待;無鎖結構中總有執行緒能逐步前進,wait-free 更保證每個執行緒都能獨立前進
- 強健性 (robustness):持有鎖的執行緒若被終止,資料結構將永久損壞;無鎖結構中某執行緒中途死亡,只丟失它自己的資料,其他執行緒照常運作
- 天然無死結:沒有任何鎖,死結 (deadlock) 不可能發生
代價:不變量的維護更難
沒有鎖就無法「鎖住整個結構再修改」。必須用原子操作限制修改、注意其他執行緒看到修改的順序(記憶體順序),否則就是資料競爭與未定義行為。
無鎖結構的弊
- 可能活結 (livelock):兩個執行緒同時修改,彼此的修改都讓對方重來,雙方不斷重試——如同兩人同時過獨木橋,在中間相遇又都退回。活結依賴執行緒排程,通常短暫存在、只損耗效能,不像死結是永久問題
- 複雜度高:wait-free 演算法比阻塞式演算法複雜得多,即使無人競爭也可能需要更多步驟
- 原子操作昂貴:原子操作比非原子操作慢,「原子操作就相當於無鎖結構中的鎖」;硬體還必須透過同一原子變數同步各執行緒,形成快取乒乓 (cache ping-pong) 瓶頸(見第 8 章)
- 整體效能未必更好:無鎖降低了單一執行緒的等待時間,但可能拉低整體吞吐——提交前務必以最壞等待時間、平均等待時間、整體執行時間等指標實測
「無鎖結構不會死結」有例外情境要注意
無死結不代表無活結;且 wait-free 依定義步驟有上限,才真正免疫活結。另外,某些平台上 std::atomic<> 的實作內部使用鎖(可用 is_lock_free() 檢查),此時「無鎖」演算法實際上基於鎖完成。
決策準則
使用無鎖結構的兩大理由:最大化併發與強健性。若只是想「更快」,請先量測——基於鎖的結構常常更快也更容易寫對。
Exam/Test Patterns
| 情境/關鍵字 | 答案 |
|---|---|
| 「自旋鎖是無鎖結構嗎?」 | 不是——它非阻塞,但仍是鎖(非阻塞—有鎖) |
| 「其他執行緒都暫停時才保證完成」 | obstruction-free(無阻礙) |
| 「保證至少一個執行緒有進展」 | lock-free(無鎖) |
| 「每個執行緒都在有限步驟內完成」 | wait-free(無等待) |
| 「CAS 迴圈重試無上限」是哪一級? | lock-free 但非 wait-free(可能飢餓) |
| 「執行緒持鎖時被 kill,結構壞掉」 | 無鎖結構的強健性優勢:資料不會因此損壞 |
| 「兩執行緒互相讓對方重試、無限循環」 | 活結 (livelock);無鎖結構可能發生,wait-free 不會 |
| 「無鎖一定比有鎖快?」 | 否——原子操作與快取乒乓可能拉低整體效能,必須實測 |
| 「怎麼確認 std::atomic 真的無鎖?」 | 呼叫 is_lock_free()(某些平台內部用鎖) |
Related Notes
- 07-Lock-Free-Data-Structures/02-Lock-Free-Stack
- 07-Lock-Free-Data-Structures/03-Memory-Reclamation-Strategies
- 07-Lock-Free-Data-Structures/04-Lock-Free-Queue-and-Memory-Ordering
- 07-Lock-Free-Data-Structures/05-Lock-Free-Design-Guidelines
- 07-Lock-Free-Data-Structures/Practice-Lock-Free-Data-Structures
- 05-Memory-Model-and-Atomics/02-Atomic-Types-and-Operations
- 06-Lock-Based-Data-Structures/01-Concurrent-Design-Guidelines
- 11-Testing-and-Debugging/01-Concurrency-Bug-Types