無鎖作為目標(lock-free)
鎖有個隱藏的脆弱:如果持鎖的執行緒被暫停了——被排程器搶佔,或更糟,當掉了——所有等待那把鎖的其他執行緒也都卡住。一個停滯的執行緒能凍結整個系統。無鎖是這樣一個目標:打造完全不需要鎖的共享資料結構,使任何單一執行緒的延遲都無法阻塞其他人。它是演算法的一種性質,而不只是沒出現「lock」這個字。
精確地說,一個演算法是無鎖的,若只要執行緒跑得夠久,就總有至少一個執行緒在前進——整個系統永遠不會卡死,即使個別執行緒被任意延遲。(更強的性質「無等待(wait-free)」保證每個執行緒都在有限步數內前進;較弱的「無障礙(obstruction-free)」只保證執行緒單獨跑時能前進。)無鎖程式碼幾乎完全由原子操作建成,尤其是比較並交換:執行緒在本地準備好它的改動,然後試著用單一原子 CAS 提交;若有別的執行緒先到了,CAS 就失敗,它便以最新狀態重試。沒有任何臨界區間能讓執行緒「卡在裡面」,因為根本沒有鎖可持——最糟也不過是執行緒重跑它的 CAS 迴圈。
無鎖結構(佇列、堆疊、雜湊表)在「持鎖執行緒被停滯或搶佔會無法接受」之處最為重要:即時系統、作業系統核心,以及爭用極高的熱路徑。但誠實的告誡份量很重,這也是為什麼本條目只點出這個目標、由第二卷做真正的處理。無鎖程式碼確實很難寫對——它得對付 ABA 問題、微妙的記憶體順序需求,以及安全的記憶體回收(你不能釋放一個別的執行緒可能還在讀的節點)。它也不會自動更快:在重度爭用下,CAS 重試迴圈可能白做工,而一把設計良好的鎖往往比手刻的無鎖結構更簡單也更快。只有在有量測過的理由與極大的謹慎下,才動用無鎖;預設請用互斥鎖。
無鎖計數器就只是每個執行緒做 atomic_fetch_add(&count, 1)——沒有鎖,被暫停的執行緒也擋不住其他人。無鎖佇列則難得多:它需要 CAS 迴圈,外加一套安全回收已移除節點的機制。
無鎖意味著沒有任何執行緒的延遲能阻塞其餘的;系統總會有些進展。
無鎖並不等於「沒有互斥鎖所以更快」——它是一個精確的進展保證,而天真的無鎖程式碼很容易出現微妙的錯誤(ABA、記憶體順序、回收期間的釋放後使用)。對大多數應用程式碼,設計良好的互斥鎖才是對的預設;把無鎖留給量測過、有充分理由的熱路徑。