比較並交換(compare-and-swap)
測試並設定威力強大卻很鈍:它總是覆寫。比較並交換(CAS)是更精緻的原子指令,也是現代多數無鎖程式設計背後的主力。它作為一個不可分割步驟所做的事是:「看一下這個記憶體位置;只有當它還保有我預期的值時,才把它換成這個新值,並告訴我你換了沒有。」這就像「只有在你上次看過之後沒人改動」的前提下才更新共用白板——並且原子地得知你的更新到底有沒有成功貼上去。
具體而言,CAS(address, expected, new) 原子地做這件事:讀出 address 目前的值;若它等於 expected,就存入 new 並回報成功;否則保持不變並回報失敗(常以回傳它實際找到的值來表示)。經典用法是一個重試迴圈。要在不用鎖的情況下原子地把計數器加一:把目前的值讀進 old;算出 new = old + 1;再做 CAS(counter, old, new)。若成功,就大功告成。若失敗,表示這段期間有別的執行緒改了計數器,於是你重讀再試一次。迴圈持續旋轉,直到某次嘗試落在一個未被改動的值上——而關鍵在於,每一次失敗都意味著別人有了進展,所以整個系統作為一體永遠在前進。
CAS 比測試並設定更具表達力,因為它能「依目前的值有條件地更新」,這正是讓你不僅能造出鎖、還能造出永不阻塞的無鎖堆疊、佇列與計數器的本錢。兩點誠實的提醒。第一,CAS 在高度爭用下可能徒勞失敗、把工夫浪費在重試上,所以它並非自動就比鎖快。第二,它有個著名的陷阱:ABA 問題。CAS 只檢查值是否等於 expected,並不檢查它「中間是否從未改變」;如果值從 A 變到 B 再變回 A,CAS 仍會欣然成功,儘管世界已在它腳下挪移過。解決 ABA 需要額外機制,例如版本標記。儘管如此,CAS 仍是並行程式設計中最重要的單一原子原語。
對堆疊做無鎖的 push:把目前的 top 讀進 old;設 new_node.next = old;再做 CAS(top, old, new_node)。若這段期間有別的執行緒也 push 了,top 就不再等於 old,CAS 失敗,於是你只要重讀 top 再試一次——從頭到尾沒有持有任何鎖。
CAS = 只有在未被改動時才更新;失敗就重試。無鎖程式碼的引擎。
CAS 只檢查值、不檢查歷程:從 A 變 B 再變回 A 也會通過(ABA 問題)。而且在爭用下,無止盡的重試可能讓 CAS 比一把普通的鎖還慢。