互斥鎖(mutex lock)
互斥鎖(mutex,是 mutual exclusion 的縮寫)是保護臨界區間時日常又實用的工具。把它想成共用房間旁掛著的一把唯一的鑰匙:進去前你先把鑰匙拿走(取得/acquire);你持有它的期間,別人都進不來;離開時你把它掛回去(釋放/release),下一個人才拿得到。整套規則就只是紀律:每一條想碰共用資料的執行緒,都必須先取得同一把互斥鎖、用完再釋放。只要一以貫之地使用,你就能得到互斥,而完全不必去想原子指令。
互斥鎖提供兩個操作,慣例上叫取得(acquire/lock)與釋放(release/unlock)。acquire 在鎖空閒時立刻成功,否則讓呼叫者等待到它空出來;release 把它標記為空閒,並讓一個等待者前進。在內部,acquire 是用測試並設定或比較並交換這類硬體原子指令造出來的,好讓「檢查是否空閒並一把抓住」這個步驟本身就不可分割。關鍵在於,一個正確實作的互斥鎖也帶有記憶體順序保證(acquire/release 語意),使臨界區間內所做的一切寫入,都對下一條取得它的執行緒可見——於是你用一個工具同時得到了互斥與正確的可見性。
兩個實務上的真相。第一,互斥鎖只憑「約定」來保護資料:它是一面旗標,而不是圍在變數外的圍籬。只要有一條程式路徑沒持有互斥鎖就改動了共用資料,保護就沒了。互斥鎖與它所守護的資料,只在程式設計者的腦中相連,所以這份紀律必須完美無缺。第二,互斥鎖有兩種等待風格,選錯會很痛。自旋風格的互斥鎖會忙碌等待(自旋),在等待會非常短時才對;阻塞式互斥鎖則讓等待的執行緒睡著,在等待可能很長時才對,因為睡著能把 CPU 讓給其他工作。忘記釋放互斥鎖(尤其在提早 return 或例外發生時)是死結的經典來源。
acquire(m); balance = balance + deposit; release(m)。每一條碰觸 balance 的執行緒,都用同一把互斥鎖 m 把它的存取夾起來,於是這個「讀取-修改-寫回」永遠不會和另一條執行緒的交錯。只要有一處忘了 acquire,整個保證就崩潰。
取得 -> 碰共用資料 -> 釋放。永遠是同一把互斥鎖,永遠成對。
互斥鎖是一種約定,不是一道牆:它只有在每個存取者都上鎖時才守得住資料。忘記釋放(在提早 return 或例外時)是死結的頭號成因。