載入連結/儲存條件(load-linked / store-conditional)
/ L-L / S-C /
想像借一本帶絆線的圖書館書:你把書借出(一個感測器開始盯著它),做你的筆記,當你想歸還你編輯過的副本時,系統只在你持有期間沒人碰過那本書時才接受。若有人動過它,你的更新就被拒絕,你得重來。載入連結/儲存條件(load-linked / store-conditional)正是建立在這個想法上的一對指令:載入一個值的同時在它的位址上放一個隱藏的保留,稍後試著儲存——但儲存只在這中間沒有人寫過那個位址時才成功。
它是兩條指令。載入連結(LL)讀取一個記憶體位置,並悄悄為這個核心在它上面設一個保留。核心接著可隨意算出一個新值。儲存條件(SC)試圖把新值寫回,但只在保留仍然完好時才成功——也就是只在自 LL 以來沒有別的核心(也沒有中斷)寫過那個位址時;若有,SC 就失敗、什麼都不寫,並回傳一個失敗旗標。於是讀-改-寫變成:LL 那個值、計算、SC 那個結果,若 SC 失敗就迴圈重試。那個保留由快取一致性硬體追蹤:另一個核心對那條列的寫入會清掉你的保留。
LL/SC 是 RISC 世界對 x86 上 CAS 所解決的同一問題的答案,而且它避開了 CAS 的 ABA 缺陷:SC 在對該位址的任何插入寫入時都失敗,即使是那種把原值還原回去的寫入,因為它盯的是位址本身、而非比較一個值。RISC-V、ARM、MIPS、PowerPC 這些架構都提供 LL/SC(RISC-V 稱之為 LR/SC)。誠實的提醒:那個保留很脆弱——在某些實作上,一次情境切換、一個中斷、或一次不相干的存取都可能清掉它——所以 SC 可能假性失敗,而 LL/SC 區段必須保持極小,這就是為什麼更高階的原子操作和鎖建立在它之上,而非直接生用。
用 LL/SC 做原子遞增:retry: r = LL(&counter);r = r + 1;ok = SC(&counter, r);if (!ok) goto retry。若另一個核心在 LL 與 SC 之間寫了 counter,保留就被清掉、SC 回傳失敗、我們迴圈。注意:即使另一個核心寫回了同樣的值,它也會失敗——不像比較數值的 CAS,所以沒有 ABA 問題。
LL/SC 盯的是位址、不是值,所以任何插入的寫入都會讓儲存失敗——俐落地避開了咬住 CAS 的 ABA 陷阱。
儲存條件可能假性失敗(來自中斷、情境切換、甚至某些晶片上的不相干存取),所以 LL/SC 永遠用在重試迴圈裡,而 LL 與 SC 之間的程式碼必須保持非常短。