鎖不變式
鎖本身什麼都不保護——它只是執行期會去翻動的一個旗標。讓它有用的,是你在自己腦中立下的承諾:「這把特定的鎖守護這份特定的資料,而每一次存取那份資料都在持有這把鎖時發生。」這個承諾就是鎖不變式。編譯器從不檢查它;它完全活在你的設計與註解裡。做對了,你的資料就安全;只要在一處破壞它,鎖就變成裝飾品。
其實這裡有兩個交織的概念。第一是關聯:每一份共享可變狀態都指定一把鎖,規則是任何執行緒不得在未持有那把鎖時讀寫那份狀態。第二是一致性不變式:一個關於資料、在鎖未被持有時恆為真的陳述——以有界緩衝區為例,也許是「0 <= count <= capacity 且確實存放著 count 個項目」。持鎖的執行緒在工作期間可以暫時打破不變式,但它必須在解鎖前把它復原,好讓下一個取得鎖的執行緒總是看到一致狀態的資料。持鎖就是你的私人窗口,讓一個雜亂的多步驟修改在別人眼中看起來像是不可分割的。
用鎖不變式來思考,是並行程式設計中最有用的單一習慣,因為它把一個模糊的擔憂(「這是執行緒安全的嗎?」)變成一條可檢查的規則(「對這個欄位的每一次存取,都持有鎖 m 嗎?」)。大多數並行錯誤,要嘛是某次存取溜過了鎖,要嘛是某個提早的 return 在不變式還破著時就解了鎖。把不變式寫下來——哪怕只是鎖旁的一行註解——正是讓你和審查者能靠目視就驗證它的關鍵。
一行像 /* 由 m 保護:head、tail、count;不變式 count == (tail - head + N) % N */ 的註解,同時說明了關聯(鎖 m 保護哪些欄位)與每當 m 空閒時必須成立的一致性規則。
點名這把鎖、點名它守護的資料,並寫下鎖空閒時成立的規則。
鎖與資料的關聯從來不由語言強制——一次沒上鎖的脫線存取照樣能編譯,甚至在測試時看似正常,卻會在高負載下弄壞資料。請把不變式寫成文件,並且絕不在它破著時解鎖(例如在更新一半時提早 return)。