CAS 失敗活結與指數退避(CAS-failure livelock and exponential backoff)
無鎖重試迴圈聽起來很穩健——我的 CAS 失敗了,那就再試一次。但想像許多執行緒全在猛敲同一個原子變數:每個都讀值、算出更新、爭著用 CAS 裝上去。每一輪只有一個能贏;其他每個執行緒的 CAS 都失敗、繞回去、立刻重讀、再爭一次。在重競爭下,這些執行緒可能幾乎把全部時間花在失敗與重試上,幾乎不做有用的工作。這就是類似活結的退化——系統忙碌卻幾乎不前進——也是樂觀重試迴圈的陰暗面。
嚴格說,純 CAS 迴圈仍是無鎖的,所以這不是形式意義上的真活結(每一輪總有人贏,整個系統確實在前進)。但實務效果很殘酷:當你加入更多執行緒時,吞吐量可能崩跌而非上升,因為每次 CAS 嘗試也產生快取一致性(cache-coherence)流量——每次寫入那條被爭奪的快取列,都會讓它在其他每個核心的快取中失效,於是所有自旋的執行緒不斷把一條快取列在彼此之間乒乓。爭奪者越多,浪費的頻寬越多,每次成功對應的失敗嘗試也越多。這有時稱為競爭崩潰(contention collapse)或重試風暴(retry storm)。
標準解法是指數退避(exponential backoff):一次 CAS 失敗後,不立刻重試,而是等一小段隨機化的延遲,並在每次接連失敗後把最大延遲加倍(設一個上限)。把重試在時間上散開,能大幅減少同時碰撞,於是每次重試成功的機率高得多,那條快取列被碰的頻率也低得多。隨機化(抖動 jitter)至關重要——沒有它,退避後的執行緒會重新同步、再次步調一致地碰撞。退避以低競爭下一點點額外延遲,換取高競爭下大得多的吞吐量;它也是把無障礙演算法變成在實務上能良好前進的那個經典競爭管理器。
/* 帶指數退避與抖動的 CAS 迴圈,用來馴服競爭。 */ unsigned delay = 1; while (!CAS(&shared, old, neu)) { for (unsigned i = 0; i < delay; i++) cpu_relax(); /* 短暫暫停 */ delay = min(delay * 2, MAX_DELAY); /* 進一步退避 */ delay ^= rand_jitter(); /* 去同步化 */ old = atomic_load(&shared); neu = f(old); /* 重讀、重算 */ }
每次失敗後退避並加上隨機抖動,把重試在時間上散開,使碰撞與快取列乒乓大幅減少。
純 CAS 重試迴圈是無鎖的、形式上不算活結,但在重競爭下它可能遭遇競爭崩潰,吞吐量隨執行緒增加而下降。解藥是退避加抖動,而非更多自旋。