嘿,小伙伴们,我是小康 👋
说实话,死锁是多线程编程中最让人头疼的Bug之一。它不像段错误那样会立即崩溃,而是悄无声息地让程序"卡死",CPU不高、内存不涨,就是不动了...
今天,我就来盘点一下实战中最常见的7种死锁场景,看看你的代码中了几个?
这是最经典、也是最常见的死锁场景。
std::mutex mtx1, mtx2;voidthread1_func(){std::lock_guard<std::mutex> lock1(mtx1); // 先锁mtx1std::this_thread::sleep_for(std::chrono::milliseconds(100));std::lock_guard<std::mutex> lock2(mtx2); // 再锁mtx2// 业务逻辑...}voidthread2_func(){std::lock_guard<std::mutex> lock2(mtx2); // 先锁mtx2std::this_thread::sleep_for(std::chrono::milliseconds(100));std::lock_guard<std::mutex> lock1(mtx1); // 再锁mtx1// 业务逻辑...}死锁原因:线程1持有mtx1等待mtx2,线程2持有mtx2等待mtx1,形成循环等待。
方案1:统一加锁顺序
voidsafe_thread_func(){// 所有线程都按照 mtx1 -> mtx2 的顺序加锁std::lock_guard<std::mutex> lock1(mtx1);std::lock_guard<std::mutex> lock2(mtx2);// 业务逻辑...}方案2:使用 std::lock 原子获取多锁(推荐)
voidsafe_thread_func(){std::lock(mtx1, mtx2); // 原子地同时获取两个锁std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);// 业务逻辑...}方案3:使用 std::scoped_lock(C++17,最推荐)
voidsafe_thread_func(){std::scoped_lock lock(mtx1, mtx2); // 自动按顺序加锁,RAII管理// 业务逻辑...}这个错误看似低级,但在实际项目中出现频率相当高!
std::mutex mtx;voidprocess_data(){ mtx.lock();if (some_error_condition) {return; // 糟糕!忘记unlock就返回了 }// 业务逻辑... mtx.unlock();}死锁原因:第一个线程触发了提前返回,锁没有释放,后续所有线程都会永久阻塞。
永远使用RAII管理锁,不要手动lock/unlock
voidsafe_process_data(){std::lock_guard<std::mutex> lock(mtx); // 自动管理,异常安全if (some_error_condition) {return; // lock_guard析构时会自动unlock }// 业务逻辑...} // 离开作用域自动unlock同一个线程对同一个普通mutex多次加锁会死锁!
std::mutex mtx;voidfunc_a(){std::lock_guard<std::mutex> lock(mtx);// 业务逻辑... func_b(); // 调用func_b}voidfunc_b(){std::lock_guard<std::mutex> lock(mtx); // 再次锁同一个mutex,死锁!// 业务逻辑...}死锁原因:std::mutex不支持递归加锁,同一线程第二次lock会阻塞自己。
方案1:使用递归锁
std::recursive_mutex mtx; // 改用递归锁voidfunc_a(){std::lock_guard<std::recursive_mutex> lock(mtx); func_b(); // 可以再次加锁,不会死锁}voidfunc_b(){std::lock_guard<std::recursive_mutex> lock(mtx);// 业务逻辑...}方案2:重构代码,避免递归锁(更推荐)
std::mutex mtx;voidfunc_a(){std::lock_guard<std::mutex> lock(mtx);// 业务逻辑... func_b_internal(); // 调用不加锁的内部函数}voidfunc_b(){std::lock_guard<std::mutex> lock(mtx); func_b_internal();}voidfunc_b_internal(){// 实际的业务逻辑,不加锁// 调用者保证已经持有锁}条件变量使用不当也会导致死锁,这个很多人不知道!
std::mutex mtx;std::condition_variable cv;bool ready = false;// 生产者voidproducer(){ ready = true; // 没有在锁保护下修改共享变量! cv.notify_one();}// 消费者voidconsumer(){std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, []{ return ready; }); // 可能永久等待!// 业务逻辑...}死锁原因:如果notify_one()在wait()之前发生,通知会丢失,消费者永久等待。
共享变量的修改必须在mutex保护下进行
std::mutex mtx;std::condition_variable cv;bool ready = false;// 生产者voidproducer(){ {std::lock_guard<std::mutex> lock(mtx); ready = true; // 在锁保护下修改 } cv.notify_one(); // 可以在锁外通知(性能更好)}// 消费者voidconsumer(){std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, []{ return ready; }); // 即使通知先到,也能检测到ready为true// 业务逻辑...}没有锁也能死锁?是的!thread::join()也可能导致死锁。
std::thread t1, t2;voidthread1_func(){// 业务逻辑... t2.join(); // 等待t2结束}voidthread2_func(){// 业务逻辑... t1.join(); // 等待t1结束}intmain(){ t1 = std::thread(thread1_func); t2 = std::thread(thread2_func);// 两个线程互相等待,死锁! t1.join(); t2.join();}死锁原因:t1在等t2结束,t2在等t1结束,形成循环等待。
不要让线程互相join,在主线程统一管理
std::thread t1, t2;voidthread1_func(){// 业务逻辑,不join其他线程}voidthread2_func(){// 业务逻辑,不join其他线程}intmain(){ t1 = std::thread(thread1_func); t2 = std::thread(thread2_func);// 在主线程按顺序join t1.join(); t2.join();}这种场景不是真正的死锁,但会让程序"看起来像死锁"。
std::mutex io_mutex;voidthread_func(){std::lock_guard<std::mutex> lock(io_mutex);// 持锁期间做耗时操作std::cout << "Processing..." << std::endl;std::this_thread::sleep_for(std::chrono::seconds(10)); // 模拟耗时操作// 其他线程会长时间阻塞}问题:虽然不是死锁,但其他线程会长时间等待,影响性能。
缩小临界区,减少持锁时间
std::mutex io_mutex;voidthread_func(){// 耗时操作放在锁外std::this_thread::sleep_for(std::chrono::seconds(10));// 只在必要时加锁 {std::lock_guard<std::mutex> lock(io_mutex);std::cout << "Processing..." << std::endl; } // 尽快释放锁}这是一个非常隐蔽的死锁场景,经常出现在需要交换两个对象数据的情况下。
classAccount {mutablestd::mutex mtx;int balance;public:voidtransfer(Account& to, int amount){std::lock_guard<std::mutex> lock1(this->mtx); // 锁自己std::lock_guard<std::mutex> lock2(to.mtx); // 锁对方this->balance -= amount; to.balance += amount; }};// 使用Account acc1, acc2;std::thread t1([&]{ acc1.transfer(acc2, 100); }); // acc1 -> acc2std::thread t2([&]{ acc2.transfer(acc1, 50); }); // acc2 -> acc1,死锁!死锁原因:t1锁定acc1再锁acc2,t2锁定acc2再锁acc1,顺序相反。
使用唯一ID确定加锁顺序
classAccount {staticstd::atomic<unsignedint> next_id;constunsignedint id;mutablestd::mutex mtx;int balance;public: Account() : id(next_id++), balance(0) {}voidtransfer(Account& to, int amount){if (this == &to) return; // 防止自己给自己转账// 根据ID决定加锁顺序std::mutex* first = (this->id < to.id) ? &this->mtx : &to.mtx;std::mutex* second = (this->id < to.id) ? &to.mtx : &this->mtx;std::lock_guard<std::mutex> lock1(*first);std::lock_guard<std::mutex> lock2(*second);this->balance -= amount; to.balance += amount; }};std::atomic<unsignedint> Account::next_id{0};更简洁的方案(C++17)
voidtransfer(Account& to, int amount){if (this == &to) return;// std::scoped_lock会自动按地址排序加锁std::scoped_lock lock(this->mtx, to.mtx);this->balance -= amount; to.balance += amount;}总结一下,要避免死锁,记住这些原则:
lock_guard或scoped_lock,不要手动lock/unlockstd::lock或std::scoped_lock:原子获取多个锁,避免顺序问题手动避免死锁虽然重要,但在复杂项目中,人工检查很容易遗漏。这时候就需要自动化的死锁检测工具!
我最近开发了一个DeadLock-Sentinel 死锁检测工具,可以:
感兴趣的话,加我微信 jkfwdkf,备注「死锁检测」
或者扫描下方二维码:

如果这篇文章对你有帮助,记得点赞、在看、转发三连!🔥
你的项目中遇到过哪些死锁场景?欢迎在评论区分享你的踩坑经历! 👇

END
作者:xiaokang1998