文章目錄
- 死鎖的四大條件與預防策略詳解
- 一、死鎖的產生條件(四個必要條件)
- 二、代碼示例
- 三、死鎖的預防手段(以 C/C++ 為例)
- 1. 破壞“循環等待” —— 統一加鎖順序(推薦)
- 2. 使用 `std::lock` 一次性加多個鎖
- 3. 使用 `try_lock` 實現非阻塞獲取資源
- 4. 超時機制(C++17 起)
- 5. 添加超時檢測或死鎖檢測邏輯(高級)
- 四、死鎖的定位手段
- 1. 代碼審查:查看是否存在資源互相等待邏輯。
- 2. 日志排查:
- 3. 使用工具定位
- 4. 打印鎖地址對應變量
- 五、小結
死鎖的四大條件與預防策略詳解
死鎖(Deadlock)是指多個線程或進程因相互等待資源而永久阻塞的現象。
一、死鎖的產生條件(四個必要條件)
- 互斥條件:資源不能被多個線程同時占用。
- 占有且等待:一個線程至少持有一個資源,同時等待其他線程所持有的資源。
- 不可剝奪:線程持有的資源在未使用完之前不能被強制剝奪。
- 循環等待:存在一個線程等待鏈,鏈中的每個線程都在等待下一個線程所持有的資源。
只要四個條件同時滿足,就可能發生死鎖。
二、代碼示例
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>// 定義兩個互斥量
std::mutex mutexA;
std::mutex mutexB;void deadlock_version() {std::thread t1([] {std::lock_guard<std::mutex> lockA(mutexA);std::cout << "[T1] Locked mutexA\n";std::this_thread::sleep_for(std::chrono::milliseconds(100));std::lock_guard<std::mutex> lockB(mutexB); // 等待 mutexBstd::cout << "[T1] Locked mutexB\n";});std::thread t2([] {std::lock_guard<std::mutex> lockB(mutexB);std::cout << "[T2] Locked mutexB\n";std::this_thread::sleep_for(std::chrono::milliseconds(100));std::lock_guard<std::mutex> lockA(mutexA); // 等待 mutexAstd::cout << "[T2] Locked mutexA\n";});t1.join();t2.join();
}void no_deadlock_version() {std::thread t1([] {std::unique_lock<std::mutex> lockA(mutexA, std::defer_lock);std::unique_lock<std::mutex> lockB(mutexB, std::defer_lock);std::lock(lockA, lockB); // 同時加鎖,避免死鎖std::cout << "[T1] Locked mutexA and mutexB safely\n";});std::thread t2([] {std::unique_lock<std::mutex> lockA(mutexA, std::defer_lock);std::unique_lock<std::mutex> lockB(mutexB, std::defer_lock);std::lock(lockA, lockB); // 同樣順序加鎖std::cout << "[T2] Locked mutexA and mutexB safely\n";});t1.join();t2.join();
}int main() {std::cout << "1. 死鎖版本開始:\n";deadlock_version();// 給死鎖版本 3 秒時間卡住(演示用)// std::this_thread::sleep_for(std::chrono::seconds(3));// std::cout << "\n2. 無死鎖版本開始:\n";// no_deadlock_version();deadlock_thread.join(); // 等待死鎖線程(實際上它可能永遠卡住)return 0;
}
🧪 輸出示例:
1. 死鎖版本開始:
[T1] Locked mutexA
[T2] Locked mutexB
(此時死鎖,程序卡住)2. 無死鎖版本開始:
[T1] Locked mutexA and mutexB safely
[T2] Locked mutexA and mutexB safely
🛠 如何調試死鎖(gdb 示例)
g++ -g deadlock_example.cpp -o deadlock_example -pthread
gdb ./deadlock_example
(gdb) run
(程序卡住時)<ctrl + c>
(gdb) thread apply all bt
(gdb) run
Starting program: /.../a.out
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
1. 死鎖版本開始:
[New Thread 0x7ffff77ff6c0 (LWP 414762)]
[T1] Locked mutexA
[New Thread 0x7ffff6ffe6c0 (LWP 414763)]
[T2] Locked mutexB
^C
Thread 1 "a.out" received signal SIGINT, Interrupt.
Download failed: Invalid argument. Continuing without source file ./nptl/./nptl/futex-internal.c.
0x00007ffff7898d71 in __futex_abstimed_wait_common64 (private=128, cancel=true, abstime=0x0, op=265, expected=414762, futex_word=0x7ffff77ff990) at ./nptl/futex-internal.c:57
warning: 57 ./nptl/futex-internal.c: No such file or directory
(gdb) info threadsId Target Id Frame
* 1 Thread 0x7ffff7e8c740 (LWP 414759) "a.out" 0x00007ffff7898d71 in __futex_abstimed_wait_common64 (private=128, cancel=true, abstime=0x0, op=265, expected=414762, futex_word=0x7ffff77ff990) at ./nptl/futex-internal.c:572 Thread 0x7ffff77ff6c0 (LWP 414762) "a.out" futex_wait (private=0, expected=2, futex_word=0x55555555b1a0 <mutex>) at ../sysdeps/nptl/futex-internal.h:1463 Thread 0x7ffff6ffe6c0 (LWP 414763) "a.out" futex_wait (private=0, expected=2, futex_word=0x55555555b160 <mutexA>) at ../sysdeps/nptl/futex-internal.h:146(gdb) thread apply all btThread 3 (Thread 0x7ffff6ffe6c0 (LWP 414763) "a.out"):
#0 futex_wait (private=0, expected=2, futex_word=0x55555555b160 <mutexA>) at ../sysdeps/nptl/futex-internal.h:146
#1 __GI___lll_lock_wait (futex=futex@entry=0x55555555b160 <mutexA>, private=0) at ./nptl/lowlevellock.c:49
#2 0x00007ffff78a0101 in lll_mutex_lock_optimized (mutex=0x55555555b160 <mutexA>) at ./nptl/pthread_mutex_lock.c:48
#3 ___pthread_mutex_lock (mutex=0x55555555b160 <mutexA>) at ./nptl/pthread_mutex_lock.c:93
#4 0x000055555555680b in __gthread_mutex_lock(pthread_mutex_t*) ()
#5 0x00005555555569be in std::mutex::lock() ()
#6 0x0000555555556a5c in std::lock_guard<std::mutex>::lock_guard(std::mutex&) ()
#7 0x00005555555554b9 in deadlock_version()::{lambda()#2}::operator()() const ()
#8 0x0000555555556705 in void std::__invoke_impl<void, deadlock_version()::{lambda()#2}>(std::__invoke_other, deadlock_version()::{lambda()#2}&&) ()
#9 0x00005555555565fb in std::__invoke_result<deadlock_version()::{lambda()#2}>::type std::__invoke<deadlock_version()::{lambda()#2}>(deadlock_version()::{lambda()#2}&&) ()
#10 0x00005555555564d6 in void std::thread::_Invoker<std::tuple<deadlock_version()::{lambda()#2}> >::_M_invoke<0ul>(std::_Index_tuple<0ul>) ()
#11 0x0000555555556436 in std::thread::_Invoker<std::tuple<deadlock_version()::{lambda()#2}> >::operator()() ()
#12 0x00005555555563be in std::thread::_State_impl<std::thread::_Invoker<std::tuple<deadlock_version()::{lambda()#2}> > >::_M_run() ()
#13 0x00007ffff7cecdb4 in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6
#14 0x00007ffff789caa4 in start_thread (arg=<optimized out>) at ./nptl/pthread_create.c:447
#15 0x00007ffff7929c3c in clone3 () at ../sysdeps/unix/sysv/linux/x86_64/clone3.S:78Thread 2 (Thread 0x7ffff77ff6c0 (LWP 414762) "a.out"):
#0 futex_wait (private=0, expected=2, futex_word=0x55555555b1a0 <mutex>) at ../sysdeps/nptl/futex-internal.h:146
#1 __GI___lll_lock_wait (futex=futex@entry=0x55555555b1a0 <mutex>, private=0) at ./nptl/lowlevellock.c:49
#2 0x00007ffff78a0101 in lll_mutex_lock_optimized (mutex=0x55555555b1a0 <mutex>) at ./nptl/pthread_mutex_lock.c:48
#3 ___pthread_mutex_lock (mutex=0x55555555b1a0 <mutex>) at ./nptl/pthread_mutex_lock.c:93
#4 0x000055555555680b in __gthread_mutex_lock(pthread_mutex_t*) ()
#5 0x00005555555569be in std::mutex::lock() ()
#6 0x0000555555556a5c in std::lock_guard<std::mutex>::lock_guard(std::mutex&) ()
#7 0x000055555555539d in deadlock_version()::{lambda()#1}::operator()() const ()
--Type <RET> for more, q to quit, c to continue without paging--
#8 0x0000555555556742 in void std::__invoke_impl<void, deadlock_version()::{lambda()#1}>(std::__invoke_other, deadlock_version()::{lambda()#1}&&) ()
#9 0x000055555555664e in std::__invoke_result<deadlock_version()::{lambda()#1}>::type std::__invoke<deadlock_version()::{lambda()#1}>(deadlock_version()::{lambda()#1}&&) ()
#10 0x0000555555556502 in void std::thread::_Invoker<std::tuple<deadlock_version()::{lambda()#1}> >::_M_invoke<0ul>(std::_Index_tuple<0ul>) ()
#11 0x0000555555556452 in std::thread::_Invoker<std::tuple<deadlock_version()::{lambda()#1}> >::operator()() ()
#12 0x00005555555563e2 in std::thread::_State_impl<std::thread::_Invoker<std::tuple<deadlock_version()::{lambda()#1}> > >::_M_run() ()
#13 0x00007ffff7cecdb4 in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6
#14 0x00007ffff789caa4 in start_thread (arg=<optimized out>) at ./nptl/pthread_create.c:447
#15 0x00007ffff7929c3c in clone3 () at ../sysdeps/unix/sysv/linux/x86_64/clone3.S:78Thread 1 (Thread 0x7ffff7e8c740 (LWP 414759) "a.out"):
#0 0x00007ffff7898d71 in __futex_abstimed_wait_common64 (private=128, cancel=true, abstime=0x0, op=265, expected=414762, futex_word=0x7ffff77ff990) at ./nptl/futex-internal.c:57
#1 __futex_abstimed_wait_common (cancel=true, private=128, abstime=0x0, clockid=0, expected=414762, futex_word=0x7ffff77ff990) at ./nptl/futex-internal.c:87
#2 __GI___futex_abstimed_wait_cancelable64 (futex_word=futex_word@entry=0x7ffff77ff990, expected=414762, clockid=clockid@entry=0, abstime=abstime@entry=0x0, private=private@entry=128) at ./nptl/futex-internal.c:139
#3 0x00007ffff789e7a3 in __pthread_clockjoin_ex (threadid=140737345746624, thread_return=0x0, clockid=0, abstime=0x0, block=<optimized out>) at ./nptl/pthread_join_common.c:102
#4 0x00007ffff7cece33 in std::thread::join() () from /lib/x86_64-linux-gnu/libstdc++.so.6
#5 0x000055555555559c in deadlock_version() ()
#6 0x00005555555558ee in main ()
會看到兩個線程分別卡在獲取 mutexB 和 mutexA 的地方,形成典型的死鎖等待鏈。
🔍 解讀 GDB 棧:
🧵 Thread 2
#6 std::lock_guard<std::mutex>::lock_guard(std::mutex&) ()
#7 deadlock_version()::{lambda()#1}::operator()() const ()
線程 2 正在嘗試加鎖 mutex
:
futex_word=0x55555555b1a0 <mutex>
說明線程 2 被阻塞在 mutex
上。
🧵 Thread 3
#6 std::lock_guard<std::mutex>::lock_guard(std::mutex&) ()
#7 deadlock_version()::{lambda()#2}::operator()() const ()
線程 3 正在嘗試加鎖 mutexA
:
futex_word=0x55555555b160 <mutexA>
說明線程 3 被阻塞在 mutexA
上。
🧵 Thread 1(主線程)
#4 std::thread::join()
#5 deadlock_version()
主線程正在等待 thread 2 和 thread 3 退出,因此處于阻塞中。
通過 gdb thread apply all bt
看到:
- 兩個線程分別阻塞在
mutex
和mutexA
上。 - 推測這兩個鎖是對方線程正在持有的。
- 主線程卡在
join()
,等待死鎖線程結束。 - 完全滿足“循環等待”的死鎖條件。
三、死鎖的預防手段(以 C/C++ 為例)
1. 破壞“循環等待” —— 統一加鎖順序(推薦)
- 所有線程按固定順序加鎖,比如先加
mutex
再加mutexA
std::mutex m1, m2;void threadA() {std::lock_guard<std::mutex> lock1(m1);std::this_thread::sleep_for(std::chrono::milliseconds(100));std::lock_guard<std::mutex> lock2(m2); // 始終按照 m1->m2 加鎖順序// 臨界區
}void threadB() {std::lock_guard<std::mutex> lock1(m1); // 避免 m2->m1 加鎖順序std::this_thread::sleep_for(std::chrono::milliseconds(100));std::lock_guard<std::mutex> lock2(m2);
}
2. 使用 std::lock
一次性加多個鎖
void safe_function() {std::unique_lock<std::mutex> lock1(m1, std::defer_lock);std::unique_lock<std::mutex> lock2(m2, std::defer_lock);std::lock(lock1, lock2); // 避免死鎖
}
使用 std::lock(mutex1, mutex2)
+ std::lock_guard
:
std::lock(mutex, mutexA);
std::lock_guard<std::mutex> lk1(mutex, std::adopt_lock);
std::lock_guard<std::mutex> lk2(mutexA, std::adopt_lock);
3. 使用 try_lock
實現非阻塞獲取資源
void try_lock_example() {if (m1.try_lock()) {if (m2.try_lock()) {// 成功獲取兩個鎖m2.unlock();}m1.unlock();}
}
4. 超時機制(C++17 起)
std::timed_mutex tmutex;void timed_lock_example() {if (tmutex.try_lock_for(std::chrono::milliseconds(100))) {// 成功獲得鎖tmutex.unlock();} else {// 獲取失敗,避免死鎖}
}
5. 添加超時檢測或死鎖檢測邏輯(高級)
待補充~
四、死鎖的定位手段
1. 代碼審查:查看是否存在資源互相等待邏輯。
2. 日志排查:
- 添加鎖操作日志(獲取、釋放時間、線程ID)。
- 利用 RAII 包裝鎖并記錄日志。
struct LockLogger {std::mutex& m;std::string name;LockLogger(std::mutex& mutex, const std::string& n) : m(m), name(n) {std::cout << "Locking " << name << std::endl;m.lock();}~LockLogger() {std::cout << "Unlocking " << name << std::endl;m.unlock();}
};
3. 使用工具定位
gdb
+thread apply all bt
:查看線程調用棧,分析是否互相等待。pstack <pid>
:打印所有線程棧。strace -p <pid>
:查看系統調用是否卡在futex
(用戶態互斥)上。valgrind
的helgrind
模塊:檢測數據競爭和死鎖。
valgrind --tool=helgrind ./your_program
4. 打印鎖地址對應變量
std::cout << &mutex << std::endl;
std::cout << &mutexA << std::endl;
運行時查看地址是否和 GDB 中 0x55555555b1a0
和 0x55555555b160
一致,可進一步確認鎖對象。
五、小結
內容 | 說明 |
---|---|
死鎖四條件 | 互斥、占有且等待、不可剝奪、循環等待 |
常見預防方式 | 固定加鎖順序、std::lock 、try_lock 、timed_mutex |
定位手段 | 日志追蹤、gdb/pstack/valgrind、調試器分析調用棧 |