原子操作(atomic operation)
Atomic 源自希臘文的「不可分割」。原子操作是這樣一種操作:就其他任何執行緒所能察覺的範圍而言,它一次全部發生——沒有可觀察的中間狀態。它就像撥動電燈開關:不是關就是開,絕不會卡在半途。這之所以重要,是因為我們寫成一行程式碼的操作,對硬體而言通常不是單一步驟。熟悉的 count = count + 1 其實會編譯成三個機器步驟:把 count 從記憶體讀進暫存器、加一、把暫存器寫回。若兩個執行緒交錯這些步驟,兩者可能讀到同一個舊值、又寫回同一個新值,於是一次遞增被悄悄遺失。原子操作的全部用意,就是讓這樣的更新成為不可分割,使這種事不可能發生。
現代 CPU 以硬體元件提供少數幾條原子指令。最基本的是原子的讀-改-寫,例如取後加(fetch-and-add,原子地讀一個值並對它加)或測試並設定(test-and-set,原子地讀一個旗標並設定它)。最重要也最通用的是比較並交換,寫作 CAS(address, expected, new):它原子地檢查 address 處的記憶體是否仍持有 expected,唯有如此才寫入 new,並回報是否成功。CPU 在記憶體匯流排或快取層級強制這種不可分割性,因此沒有別的核心能在中途偷塞一個衝突的存取。程式語言把這些暴露為原子型別(例如 C 或 C++ 的原子整數,或 Java 的 AtomicInteger),其遞增、交換與比較並交換等方法都保證是原子的。
原子操作是並行裡其他一切的根基。鎖本身就是用原子的測試並設定或比較並交換實作的;無鎖資料結構則直接用原子操作以完全避開鎖。但有兩點誠實的提醒。第一,單一操作的原子性,並不會讓一連串操作變成原子的:做兩個分開的原子更新,並不等於同時更新兩者——那仍需要鎖或交易。第二,原子性講的是不可分割性,這和可見性順序是兩回事的保證;在寬鬆記憶體模型上,你可能還需要記憶體屏障來控制其他執行緒何時看到結果。原子的意思是「全有或全無」,並不自動代表「立刻被所有人依序看到」。
缺乏原子性時的遺失更新:count = 0。執行緒 A 讀到 0、執行緒 B 讀到 0,A 算出 1 並寫入 1,B 算出 1 並寫入 1。兩次遞增,count 卻是 1。原子的取後加讓每次遞增不可分割,結果便正確地是 2。
原子操作存在就是為了防止這種遺失更新的競態——看似一行的原始碼,其實是三個機器步驟在交錯。
單一指令的原子性不能組合:先後做的兩個原子操作並非整體原子,另一個執行緒可以觀察到兩者之間的狀態。要把多個更新捆成一個不可分割的整體,仍需要鎖或交易記憶體。