?? 并發五大常見陷阱
目錄
- 數據競爭 (Data Race)
- 死鎖 (Deadlock)
- 競態條件 & 餓死現象 (Race Condition & Starvation)
- 懸掛指針 (Dangling Pointer)
- 重復釋放 (Double Free)
- 開發自查清單
1. 數據競爭 (Data Race)
-
專業定義
兩個及以上線程在缺乏同步的情況下同時訪問同一地址,并且至少有一個線程執行寫操作。
-
通俗比喻
兩個人同時在同一張 Excel 表里改同一個單元格,最后單元格的內容誰也說不準。
-
危害
狀態錯亂、隨機崩潰、難以復現。 -
常見場景
- 全局計數器
counter++
- 緩存讀寫(讀線程沒加鎖)
- 全局計數器
-
極簡示例(C11)
int counter = 0;void* add(void* _) {for (int i = 0; i < 1000000; ++i) counter++;return NULL; }
-
發現方法
ThreadSanitizer、Helgrind、Rust 編譯器借用檢查。 -
避免策略
互斥鎖Mutex
、原子類型Atomic*
、消息傳遞 (channel),“不共享即不競爭”。
2. 死鎖 (Deadlock)
-
專業定義
多線程因為循環等待資源導致相互阻塞且永不釋放。 -
通俗比喻
A 把門鑰匙握在手里等 B 的車鑰匙,B 把車鑰匙握在手里等 A 的門鑰匙——誰也別想走。 -
死鎖四必要條件
互斥、占有并等待、不可搶占、循環等待。 -
典型示例(Java)
synchronized(lockA) {synchronized(lockB) { /* … */ } }// 另一線程 synchronized(lockB) {synchronized(lockA) { /* … */ } }
-
發現方法
jstack
看線程棧、Rustcargo-deadlock
、Go 運行時死鎖檢測。 -
避免策略
固定鎖順序、try_lock
+ 超時回退、細粒度鎖、用 Channel / Actor 模式取代共享鎖。
3. 競態條件 & 餓死現象
3.1 競態條件 (Race Condition)
-
專業定義
程序的正確性依賴于多個事件的執行順序,而該順序又未受控制。 -
通俗比喻
檢查房門沒鎖 → 去拿快遞 → 回來發現小偷已進屋——檢查?后?執行窗口被搶占。 -
場景示例(Shell)
if [ ! -e /tmp/mydir ]; thenmkdir /tmp/mydir # 另一個進程可能在檢查后立即創建 fi
-
修復關鍵
將“檢查 + 創建”做成一個原子操作(加鎖,或用mkdir -p
讓系統保證原子性)。
3.2 餓死現象 (Starvation)
-
定義
線程長期得不到 CPU 時間片或資源,處于“活著卻干不了活”狀態。 -
典型場景
- 讀寫鎖:大量讀鎖使寫鎖一直被餓死
- 嚴格優先級調度:低優先級線程永遠排不到
-
緩解
公平鎖、優先級繼承、動態調度策略。
4. 懸掛指針 (Dangling Pointer)
-
專業定義
指針仍指向一塊已釋放或作用域結束的內存區域。 -
通俗比喻
拿著老房子的鑰匙,房子已被拆遷,再開門只會撞墻。 -
示例(C)
int *p; {int x = 42;p = &x; } // x 生命周期結束 printf("%d\n", *p); // 懸掛訪問
-
解決思路
RAII / 智能指針 / Rust 所有權模型自動禁止懸掛引用。
5. 重復釋放 (Double Free)
-
專業定義
對同一內存塊調用兩次釋放操作。 -
通俗比喻
電影票撕過一次又被檢票員再撕一次——第二次可能撕到別人的票。 -
示例(C)
char *buf = malloc(100); free(buf); free(buf); // 第二次釋放
-
危害
崩潰;更嚴重時可破壞堆結構,被黑客利用執行任意代碼。 -
防范
設置指針為NULL
、使用智能指針(unique_ptr
/ Rust 所有權自動回收)、開啟 AddressSanitizer。
6. 開發生命線——五步自查
- 代碼審查:重點看“檢查?后?執行”與手動內存管理片段。
- 靜態分析:開啟編譯器最大警告,IDE 并發檢查。
- 動態探測:CI 跑 AddressSanitizer / ThreadSanitizer / Valgrind。
- 運行監控:鎖等待時間、線程阻塞時長、隊列深度。
- 語言特性先行:能用 RAII / 智能指針 / 所有權就別手寫
malloc/free
。
一句話總結
“共享越少,風險越小;讓編譯器和工具兜底,用良好設計把錯堵在出生之前。”