某些操作一旦開始,就必須在其他執行者眼中像一個完整的整體。
這就是今天要談的:
Atomic 中文常翻成:原子性。
而 Atomic Operation 則常被描述成:不可分割的操作。
對其他會觀察或操作同一份共享狀態的執行者而言,這個操作不會暴露一個「只完成一半」的中間狀態。
假設有一個 Atomic Increment:
Thread A
──────────[ Atomic Operation ]──────────
↑ ↑
Before After
對其他會競爭同一份資料的 Thread 而言,這個操作在效果上像是一個整體。
Source Code 的一行,不等於一個 Atomic Operation。
一個counter++ ; 經過 Compiler 之後,可能產生多個 Machine Instructions。
可能像:
load counter → register
add register, 1
store register → counter
(2)Read-Modify-Write
先讀取舊值
↓
修改
↓
把新值寫回
以上這整組操作必須具有某種不可被其他競爭操作破壞的 **Atomicity。**為了防止Race Condition,確保在多執行緒或多處理器環境下資料的一致性與正確性。
如果我們只是用普通的:Load / Add / Store
那軟體不能憑空說從今天開始這三個動作就是 Atomic
因為另一顆 CPU Core 仍然可能同時操作相同的 Memory。
因此現代 CPU 會提供一些能支援同步的 Atomic Instructions / Atomic Read-Modify-Write Operations。
不同 CPU Architecture 提供的機制不同。其中一個非常經典的概念是:Compare-And-Swap
只有當某個 Memory Location 目前仍然等於我預期的舊值時,才把它改成新值,而且「比較 + 更新」要以 **Atomic **的方式完成。
(2)為什麼一定要 Atomic?
假設沒有 Atomic CAS。
Thread A:檢查 counter == 10 → Yes
就在這時 Thread B:counter = 20
Thread A 回來後還按照剛才的結果:寫入 11
於是 Thread B 的修改又被覆蓋掉了。
所以如果我們需要:「確認目前值」+「根據確認結果修改」
成為一個不可被競爭者破壞的邏輯單位,就需要 **Atomic Read-Modify-Write **的支援。
不是。
以前在談 Single-core OS Kernel 時,可能會看到某些情境使用:Disable Interrupt
目前 CPU
│
Disable Interrupt
│
執行 Critical Section
│
Enable Interrupt
在特定的單核心、Kernel-level 情境下,這可以避免某些 Interrupt 導致目前 CPU 上的執行被相應路徑打斷。
Core 1 Core 2
Disable Interrupt
操作 counter 操作 counter
你把:Core 1 的 Interrupt 關掉,Core 2 並沒有因此消失。
它還是可能同時操作 Shared Memory。
因此在現代 Concurrent Programming 裡,我們不能把:Atomicity 簡化成:不要被 Interrupt
每個小操作各自 Atomic,不代表多個 Atomic Operations 組成的高階邏輯也自動 Atomic。
整段邏輯,可能需要更高階的 Synchronization Design,而不是看到 atomic 就以為萬事 OK。
Atomicity 也不等於所有 Concurrency 問題都解決,Concurrency 除了 Atomicity,還會遇到其他問題。例如:Thread A 必須等 Thread B 完成某件事,這不是單純把一個變數改成 Atomic 就一定能完整表達的。
又例如之後會遇到:
所以 Atomic Operation 是同步工具的重要底層基礎
Thread A 在完成這個 Critical Section 之前,不要讓 Thread B 進來破壞它。
Thread A
lock
│
▼
┌────────────────────────┐
│ Critical Section │
│ │
│ check queue │
│ get item │
│ remove item │
│ update state │
│ │
└────────────────────────┘
│
▼
unlock
Thread B:
想進入
│
▼
Mutex 已經被 A 持有
│
▼
先不能進入
這就是:Mutex
但假設,Thread A:看到 lock == 0
Thread B:也看到 lock == 0
兩邊都覺得:我拿到鎖了!
直接完蛋。
所以 Mutex 底層的「取得鎖」本身,也必須建立在某種:Atomic Operation
一種方法可能是直接使用適合的 Atomic Operation。
我們後面看到的很多 Synchronization Mechanism,底層都離不開硬體所提供的 Atomic Support。
可能利用:
(1)Atomic Operation 對其他競爭同一共享狀態的執行者而言,不會暴露一個可以被任意介入、破壞的半完成更新。
(2)另外一點,Mutex 自己底層又需要 Atomic Primitive,才能保證兩條 Thread** 不會「同時都以為自己搶到鎖」。**
下一篇:
下一篇就可以正式拆 lock() 到底在做什麼、Mutex 鎖的不是變數是什麼意思、拿不到鎖的 Thread 去哪裡、Blocking 和 Spinning 差在哪、為什麼忘記 unlock() 會出大事。