「C 語言賦值 `a=b` 是原子的嗎?`a++` 是原子的嗎?」這是並發編程面試必考、工程必踩的坑。本文從 **原子操作定義**、**CPU 指令周期/總線周期**、**GCC 汇編實測**、**單核/多核語義差異** 四個維度,給出鐵證如山的結論:**高級語言單語句絕非天然原子,原子性取決於指令數、架構支持、編譯器優化、內存序**。
| 層級 | 定義 | 典型組成 |
| 時鐘周期 (T-state) | CPU 最小時間單位,主頻倒數 | 1 個 clock cycle |
| 總線周期 | CPU 對內存/I/O 完成一次讀/寫 | 8086: 4 T-states (T1-T4);486: 2 T-states (T1-T2) + 可選等待態 Tw |
| 指令周期 | CPU 執行一條指令總時間 | 含取指、解碼、執行、寫回等多個總線周期 |
關鍵推論:
- **單核**:單條指令不可中斷 (除中斷響應),**單指令 = 原子** (相對於同核其他線程)。多指令 = 非原子。
- **多核**:單指令可能包含**多個總線周期** (如 `lock add` 內部讀-修改-寫)。無總線鎖定時,其他核心可在總線周期間插入訪問。**需 `LOCK` 前綴 / 原子指令 / 緩存一致性協議 (MESI) 保證多核原子性**。
三、GCC 實測:賦值與自增的指令拆解
// 測試代碼 main.c
#include
void fun1() {
volatile int m, n; // volatile 防優化,強制內存訪問
m = 99;
n = m; // 賦值
}
void fun2() {
volatile int n = 10;
n++; // 自增
}
int main() { fun1(); fun2(); return 0; }
編譯查看汇編:`gcc -S -O0 main.c` (關閉優化還原語義)
fun1:
pushq %rbp
movq %rsp, %rbp
movl $99, -4(%rbp) # m = 99 (立即數存入棧)
movl -4(%rbp), %eax # 讀取 m → eax
movl %eax, -8(%rbp) # 寫入 n ← eax ★ 兩條 MOV 指令!
leave
ret
fun2:
pushq %rbp
movq %rsp, %rbp
movl $10, -4(%rbp) # n = 10
leaq -4(%rbp), %rax # 取 n 地址 → rax
incl (%rax) # 內存自增 [rax]++ ★ 一條 INC 指令 (但含讀-改-寫)
movl %eax, -4(%rbp) # (冗餘存儲)
leave
ret
鐵證結論
| C 語句 | 汇編指令數 | 單核原子性 | 多核原子性 | 說明 |
| `n = m` | 2 `movl` (讀 + 寫) | ❌ 非原子 | ❌ 非原子 | 中間可被中斷/調度 |
| `n++` | 1 `incl` (內存自增) | ✅ 單指令原子 (相對同核線程) | ❌ **非原子** (無 `lock` 前綴) | `incl` 內部含 總線讀-改-寫,多核下可被搶佔 |
關鍵細節:x86 `INC [mem]` 是**單指令但多總線周期** (讀取值、加1、寫回)。單核下不可被指令級中斷,但**多核下總線仲裁允許其他核心在讀/寫間插入訪問**,導致競態條件 (經典「丟失更新」)。
四、如何獲得真正的原子操作
1. GCC/Clang 內建原子函數 (C11 `_Atomic` / GCC `__sync` / `__atomic`)
// C11 標準原子類型 (需 )
#include
atomic_int n = 10;
atomic_fetch_add(&n, 1); // 原子 n++
int old = atomic_load(&n); // 原子讀
// GCC 內建 (廣泛兼容)
int n = 10;
__sync_fetch_and_add(&n, 1); // 原子 n++,返回舊值 (全屏障)
__sync_bool_compare_and_swap(&n, 10, 11); // CAS
// C++11 std::atomic
std::atomic n{10};
n.fetch_add(1, std::memory_order_relaxed); // 原子增
2. x86 `LOCK` 指令前綴 (汇編/內聯汇編)
// GCC 內聯汇編原子自增
__asm__ __volatile__("lock incl %0" : "+m" (n) :: "memory");
`LOCK` 鎖定總線 (或鎖定緩存行),確保讀-改-寫不可分割。
3. 互斥鎖 / 自旋鎖 (pthread_mutex / Windows CriticalSection)
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&mtx);
n = m; // 或 n++
pthread_mutex_unlock(&mtx);
通用、可移植、支持複雜臨界區,**性能開銷較大**。
五、volatile 誤區澄清
`volatile` 只保證編譥器不優化掉訪問、不重排序訪問,不提供任何原子性、內存屏障、緩存一致性保證。
- 防止編譯器將 `n++` 優化為寄存器操作再寫回。
- **不能**防止多核競態、不能防止 CPU 亂序執行、不能替代原子操作/鎖。
六、總結:原子操作判斷決策樹
高級語言語句
│
├─ 編譯為單條指令? ──否──→ ❌ 非原子 (如 a=b 兩條 MOV)
│
└─ 是 (如 n++ → INC [mem])
│
├─ 單核/單線程? ──是──→ ✅ 相對同核線程原子 (不可中斷)
│
└─ 多核/多線程?
│
├─ 指令含 LOCK/原子前綴? ──是──→ ✅ 多核原子
│
└─ 否 (普通 INC/ADD) ──→ ❌ 非原子 (總線周期間可被搶佔)
│
└─ 解決:原子內建函數 / LOCK 前綴 / 互斥鎖 / 無鎖數據結構
給工程師的鐵律
- 永不假設高級語言單語句原子,除非查閱架構手冊、編譯器文檔、汇編輸出三重驗證。
- 多線程共享變量必須用原子類型/鎖保護,`volatile` 不是線程同步工具。
- 優先用 C11 `` / C++11 `std::atomic`**,可移植、類型安全、內存序可控。
- 無鎖編程極其危險,除非你能完整推導內存模型、緩存一致性、編譯器重排序邊界,否則老誠實用鎖。
原子操作是軟硬件契約的交匯點,**只信汇編、只信硬件手冊、只信標準庫實現**,不信直覺、不信語句表象。
- 上一篇文章: 最大子序列和問題:O(N) 線性掃描法與 O(N log N) 分治法完整實現
- 下一篇文章: C語言文件讀寫全解析:文本模式 vs 二進制模式、fread/fwrite 實戰指南
Copyright ©
程式碼之家 All Rights Reserved