經典同步問題與並行程式設計

並行不變量(concurrency invariant)

不變量是關於你資料的一個敘述,理應永遠為真——一個程式信守的承諾。對銀行而言,不變量可能是「所有帳戶餘額的總和等於銀行裡的總金額」。對有限緩衝區而言,可能是「0 ≤ count ≤ N」以及「count 等於實際儲存的項目數」。在單執行緒程式裡,要守住這些承諾很容易:你也許會在一個操作的中途暫時打破不變量(先從一個帳戶扣款,再對另一個帳戶入帳,於是有那麼一刻錢似乎憑空消失),但沒有別人在看,到你完成時不變量已恢復。並行不變量就是同一個概念,在有其他執行緒盯著看時被認真對待。

並行的全部危險,恰恰就在於別的執行緒能在不變量被打破的那段短暫視窗裡偷看。一筆轉帳從帳戶 A 扣款、再對帳戶 B 入帳,途中會經過一個總和錯誤的狀態;若另一個執行緒此刻同時讀取兩邊餘額,它看到的是不可能的資料。解法是讓臨界區間——也就是不變量被暫時違反的那段程式碼——相對於其他所有人是不可分割的,通常靠跨越這段持有一把鎖。紀律是:在每一個你釋放鎖的時點都讓不變量為真,只在持有鎖時才打破它。如此一來,別的執行緒就永遠無法觀察到被打破的狀態。

用不變量來設計,是推理並行程式最有用的單一方法,遠比在腦中追蹤每一種可能的交錯可靠(交錯有指數般多)。你寫下什麼必須永遠成立,找出究竟哪幾行會短暫打破它,再確保那幾行在資料被鎖住時執行。一個正確的鎖式設計不過就是:每個碰共享資料的執行緒都用同一把鎖,且在解鎖前重建不變量。它也釐清了縮小臨界區間的目標——讓鎖住的範圍盡可能小(把慢或無關的工作移到外面),同時仍大到足以涵蓋不變量被打破的每一刻。

轉帳的不變量:total = balance[A] + balance[B] 維持固定。「balance[A] -= amount」與「balance[B] += amount」這兩個更新必須在同一把鎖下執行,因為兩者之間 total 會一時錯誤。

只在持有鎖時打破不變量,解鎖前恢復它。如此其他執行緒永遠看不到不一致的狀態。

不變量是一種設計工具,並非語言會替你強制的東西——若你不持鎖就讀共享資料,編譯器不會警告。正確性取決於每一次存取都遵守紀律;一次粗心、未受保護的讀取就可能觀察到被打破的狀態。

又称
不變式invariant並行不變式