当前位置: 首页 > news >正文

跟我学C++中级篇——控制死锁

一、同步和死锁

在前面学习多线程和网络编程时,都对线程中数据的同步和数据结构多线程访问的安全问题进行了分析和说明。其实,多线程编程之所以难,难点之一就在这里,数据同步意味着效率和安全的平衡,而这里的安全有一个重要的关节就在于死锁。
所以如果能很好的掌握线程间的同步以及防止出现死锁这些问题后,基本对多线程的理解和控制就算是入了门。

二、死锁场景

死锁其实在大多数的开发者实际的开发中遇到的并不多。即使遇到也是在应用层面上用到,很少有主动写出死锁的。但这不代表死锁少见,一般来说,常见的死锁出现的场景有以下几方面:
1、数据库中的事务处理
比如多个事务都需要同时处理多张表,这里假在锁定一些表后,有可能导致死锁
2、多线程编程
这种情况下刚刚也提到了,较少有开发者会写出这种死锁的代码,主要原因是,一般的开发场景也不会有多个资源在线程间同时访问,即使有,很多开发者也会分开处理,等待通知后再进行
3、在办公中访问一些打印机等专享资源
这种情况其实比较常见,有时候儿打印机(包括文件等)啥的与OS或网络的交互会犯傻,这时个儿往往重启一下就好了
4、分布式应用中的同步请求
这种情况在分布式应用中比较常见,比如P2P中的互相通信如果操作不当,就有可能产生死锁
5、大规模计算中的资源分配
比如在一些大规模计算中使用计算图,就有可能在图的处理中资源分配出现死锁问题

三、避免死锁

要想避免死锁就必须了妥死锁的条件,学习过操作系统相关知识的开发者都知道,死锁的四个条件:
1、资源互斥
这个比较容易理解,资源必须是独占的,即一个资源只能被一个线程占用。这就和食堂排队打汤的一样,大勺只有一个,只有一个人打好放勺子才可能另外一个人去使用
2、互相持有
也叫占有和等待,即一个线程完成一项工作需要两个以上的资源,已经占有了一个,申请另外一个时,发现另外一个资源被其它线程占有,它会继续申请而不释放自己持有的资源。这个更好理解,就比如集一些类似火花、邮票或卡之类的东西(假设每个都是唯一的),如果三个人分别持有一套的三部分,大家都会寻找其它的而不会把自己的部分放弃
3、不可剥夺
即任何一个线程持有的资源无法被其它线程强制获取。这个好理解,正常情况下,公民的财产不会被其它公民强行取得
4、循环等待
这个理解也不难,就是多个线程形成了一个环状的资源持有,即A线程持有B线程需要的资源,B线程持有C线程的需要的资源…N线程持有A线程需要的资源。这就是书本上讲的哲学家进餐的问题。
既然知道了死锁造成的原因,那么解决死锁就必须从上面的原因中找方法,只要打破任何一个条件,死锁也就不存在了,即:
1、打破资源互斥,即将资源设计为允许多个线程访问
2、打破互相持有,即所有的线程申请资源时一次性申请完成,要么成功,要么失败,也或者可以在申请前把自己持有的资源释放
3、打破不可剥夺,即允许在指定条件下获取某些线程占有的资源
4、打破循环等待,即对资源进行有序控制不允许随机申请,这样就会断开循环的链条

四、死锁的解决方案

知道了避免死锁的方法,就可以探讨解决死锁的编程方案了。一般来说,解决死锁的方案有以下几种:
1、无锁编程
这种最容易理解了,打不过就躲过嘛。无锁编程在前面分析说明了很多,此处不再展开
2、使用超时锁
这个可以打破占有和等待。在C++11中提供了一些新的锁如 std::timed_mutex 和 std::recursive_timed_mutex,看下面的例子:

#include <chrono>
#include <iostream>
#include <mutex>
#include <sstream>
#include <thread>
#include <vector>using namespace std::chrono_literals;std::mutex cout_mutex; // 控制到 std::cout 的访问
std::timed_mutex mutex;void job(int id)
{std::ostringstream stream;for (int i = 0; i < 3; ++i){if (mutex.try_lock_for(100ms)){stream << "成功 ";std::this_thread::sleep_for(100ms);mutex.unlock();}elsestream << "失败 ";std::this_thread::sleep_for(100ms);}std::lock_guard<std::mutex> lock{cout_mutex};std::cout << "[" << id << "] " << stream.str() << "\n";
}int main()
{std::vector<std::thread> threads;for (int i = 0; i < 4; ++i)threads.emplace_back(job, i);for (auto& i: threads)i.join();
}

3、可以将锁排序
用来打破循环等待等条件,这种应用比较简单,看下面的例子:

#include <iostream>
#include <mutex>
#include <thread>
#include <unistd.h>std::mutex m1;
std::mutex m2;void taskFunc(bool order) {if (order) {// reversestd::lock_guard<std::mutex> lock2(m2);sleep(2);std::lock_guard<std::mutex> lock1(m1);std::cout << "lock risky!\n";} else {// orderstd::lock_guard<std::mutex> lock1(m1);sleep(2);std::lock_guard<std::mutex> lock2(m2);std::cout << "lock safe!\n";}
}int main() {std::thread t1([]() { taskFunc(true); });std::thread t2([]() { taskFunc(false); });t1.join();t2.join();return 0;
}

上面的代码会产生死锁,可以强制进行结束。
4、一次性获取锁
在C++17中提供了std::scoped_lock,相关的代码可以查看前面的文章“跟我学C++中级篇——std::scoped_lock”
5、控制资源访问
这种就方法就比较多了,比如在Windows平台可以使用事件控制而在Linux平台上使用条件变量进行,即没有收到通知的一方不能去寻求资源的控制。典型的就是在Nginx的网络资源获取中就采用了这种控制的手段

如果在编程时没有注意,在实际的应用中出现了意外,可以使用一些工具来检查是否存在死锁:
1、使用GDB
在编译后,使用gdb:

gdb ./lockOrder
(gdb) r
Starting program: /home/fpc/qt65_project/lockOrder/lockOrder 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff4eff640 (LWP 843994)]
[New Thread 0x7ffff46fe640 (LWP 843995)]
[New Thread 0x7ffff3efd640 (LWP 843996)]
//此处如果不退出可以ctrl+C强制退出来
(gdb) info threadId   Target Id                                      Frame 
* 1    Thread 0x7ffff7317340 (LWP 843991) "lockOrder" __futex_abstimed_wait_common64 (private=128, cancel=true, abstime=0x0, op=265, expected=843995, futex_word=0x7ffff46fe910) at ./nptl/futex-internal.c:572    Thread 0x7ffff4eff640 (LWP 843994) "lockOrder" 0x00007ffff6ce57f8 in __GI___clock_nanosleep (clock_id=clock_id@entry=0, flags=flags@entry=0, req=req@entry=0x7ffff4ebf150, rem=rem@entry=0x0) at ../sysdeps/unix/sysv/linux/clock_nanosleep.c:783    Thread 0x7ffff46fe640 (LWP 843995) "lockOrder" futex_wait (private=0, expected=2, futex_word=0x55555555c160 <m1>) at ../sysdeps/nptl/futex-internal.h:1464    Thread 0x7ffff3efd640 (LWP 843996) "lockOrder" futex_wait (private=0, expected=2, futex_word=0x55555555c1a0 <m2>) at ../sysdeps/nptl/futex-internal.h:146
(gdb) 

这样就可以看到具体的死锁的情况了
2、使用valgrind并启用Helgrind工具
执行下列命令可得到相关日志:

valgrind --tool=helgrind --log-file=log.txt  ./lockOrder 

日志显示(未贴全):

Thread #3: Exiting thread still holds 1 lock
==849869==    at 0x4B572C0: futex_wait (futex-internal.h:146)
==849869==    by 0x4B572C0: __lll_lock_wait (lowlevellock.c:49)
==849869==    by 0x4B5E001: lll_mutex_lock_optimized (pthread_mutex_lock.c:48)
==849869==    by 0x4B5E001: pthread_mutex_lock@@GLIBC_2.2.5 (pthread_mutex_lock.c:93)
==849869==    by 0x485051D: mutex_lock_WRK (hg_intercepts.c:935)
==849869==    by 0x4854CEE: pthread_mutex_lock (hg_intercepts.c:958)
==849869==    by 0x109DE1: __gthread_mutex_lock(pthread_mutex_t*) (in /home/fpc/qt65_project/lockOrder/build/lockOrder)
==849869==    by 0x109E65: std::mutex::lock() (in /home/fpc/qt65_project/lockOrder/build/lockOrder)
==849869==    by 0x10A09D: std::lock_guard<std::mutex>::lock_guard(std::mutex&) (in /home/fpc/qt65_project/lockOrder/build/lockOrder)
==849869==    by 0x109456: taskFunc(bool) (in /home/fpc/qt65_project/lockOrder/build/lockOrder)
==849869==    by 0x109539: main::{lambda()#2}::operator()() const (in /home/fpc/qt65_project/lockOrder/build/lockOrder)
...

3、ThreadSanitizer (TSan)
它是集成于GCC和Clang的检测工具,使用方法如下:

g++ -fsanitize=thread -g -o lockorder main.cpp
./lockorder

4、Lttng
这个没有实验成功,暂时没找到原因。不过据说这个挺好用
5、其它
在不同的平台上可能有不同的工具,比如在Windows平台上的有名的工具Windbg和VS(ASan)等,安卓平台上也有类似的工具;另外像前面介绍的Perf Tools工具,也可以间接的辅助进行线程死锁的定位

五、总结

死锁虽然在面试时反复被问到,但在实践中真正写出来或者遇到的并没有想象的那么多。其实最主要的原因就是大多数的程序员都不会有这种开发的应用场景。但恰恰因为遇到的少,在实际中真正出现时,却不知道从何下手。
还是老规矩,把基础掌握好,会灵活的使用工具。只要发现定位了死锁的问题,就可以根据产生死锁的原因有针对的进行解决即可。没有过不了的火焰山。

相关文章:

  • Qt开发:JSON字符串的序列化和反序列化
  • 【OSG学习笔记】Day 14: 操作器(Manipulator)的深度使用
  • 基于机器学习的电影票房预测
  • 万象生鲜配送系统代码2025年4月29日更新日志
  • LeetCode 155题解 | 最小栈
  • 【Leetcode 每日一题 - 补卡】2302. 统计得分小于 K 的子数组数目
  • Linux电源管理(3)_关机和重启的过程
  • 第十六届蓝桥杯 2025 C/C++组 密密摆放
  • 探索语音增强中的多尺度时间频率卷积网络(TFCM):代码解析与概念介绍
  • AI赋能的问答系统:2025年API接口实战技巧
  • 【Redis——数据类型和内部编码和Redis使用单线程模型的分析】
  • 基于Arduino的STM32F103RCT6最小系统板的测试及串口通讯
  • 深度学习中的优化算法:基础全面解析
  • 聊聊Spring AI Alibaba的PlantUMLGenerator
  • 安装deepspeed时出现了以下的错误,如何解决CUDA_HOME does not exist
  • 【Java面试笔记:进阶】28.谈谈你的GC调优思路?
  • 解决STM32H743单片机USB_HOST+FATF操作usb文件
  • 从 Pretrain 到 Fine-tuning:大模型迁移学习的核心原理剖析
  • 实验六 文件操作实验
  • CISC与RISC详解:定义、区别及典型处理器
  • 马上评丨准入壁垒越少,市场活力越足
  • 广东省副省长刘红兵跨省任湖南省委常委、宣传部部长
  • 光明日报:回应辅警“转正”呼声,是一门政民互动公开课
  • A股三大股指小幅低收:电力股大幅调整,两市成交10221亿元
  • 从腰缠万贯到债台高筑、官司缠身:尼泊尔保皇新星即将陨落?
  • 诗词文赋俱当歌,听一听古诗词中的音乐性