概念
进程间进行通信或相互竞争系统资源而产生的永久阻塞,若无外力作用将永远处在死锁状态。
产生原因
(1)系统资源不足;(2)进程运行推进顺序与速度不同也可能导致死锁;(3)资源分配不当;
产生死锁四个必要条件:
(1) 互斥条件:就是一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程在请求其它资源而阻塞时,但是它对自己已获得的资源又保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
产生场景
线程对同一个锁反复加锁
如果同一线程对同一个锁加锁两次,那么这会导致死锁,这种情况在 使用自释放锁时经常出现,比如 c++ 11 的 lock_guard<mutex> ,如果在 lock_guard 还没释放时再次对同一个锁使用 lock_guard<mutex> ,则会立刻导致死锁。
解决办法:使用递归锁,递归锁会记录上一次对自己加锁的线程id,如果发现是当前执行线程和上一次加锁线程是同一个则不再做加锁动作。比如:unique_lock<recursive_mutex> lck(mtx)
多线程 + 多锁
如果同时存在多个线程和多个锁,而每个线程都有机会操作所有的锁,那么这就可能导致死锁,比如线程1加锁A,等待加锁B,而线程2加锁B,等待加锁A。
解决办法一:始终按照一致的顺序对多个锁进行加锁,比如有两个锁 A 和 B,那么任何线程在对 A 和 B加锁时都要按照 “先A后B” 或者 “先B后A” 的顺序进行即可。倘若实在无法确保顺序,那么可以使用c++ 11提供的 std::lock 进行同步加锁,此组件可以保证传入的多个锁可以在不死锁的情况下依次锁住。
解决方法二:使用trylock系列函数,当发现加锁失败时,立刻进行一次释放所有锁的动作,然后再次尝试刚才的加锁。
C++锁
一、互斥锁(Mutex)
1. std::mutex
含义: std::mutex 最基本的互斥锁,当一个线程占用锁时,其他线程必须等待该锁被释放。
使用场景: 当需要保护共享资源不被多个线程同时修改时使用。
2. std::recursive_mutex
含义: 递归互斥锁,允许同一个线程多次获取同一锁。
使用场景: 在递归函数中需要多次获取同一个锁的情况。
二、定时锁
1. std::timed_mutex
含义: 允许尝试锁定一定时间,如果在指定时间内没有获取到锁,则线程可以执行其他操作或放弃。
使用场景: 当你不希望线程因等待锁而无限期阻塞时使用。
2. std::recursive_timed_mutex
含义: std::recursive_timed_mutex结合了std::recursive_mutex和std::timed_mutex的特点,允许同一个线程多次加锁,并提供了尝试加锁的超时功能。
使用场景: 适用于需要递归锁定资源,并且希望能够设置尝试获取锁的超时时间的场景。这在需要防止线程在等待锁时无限阻塞的复杂递归调用中特别有用。
三、读写锁(Shared Mutex)
std::shared_mutex
含义: 允许多个线程同时读取资源,但只允许一个线程写入。
使用场景: 适用于读操作远多于写操作的情况。
四、自旋锁
自旋锁在C++标准库中没有直接提供一个专门的类型,但它可以使用原子操作,尤其是std::atomic_flag来实现。自旋锁是一种低级同步机制,适用于锁持有时间非常短的情况。与其他锁不同,当自旋锁无法获取锁时,它将在一个循环中持续检查锁的状态,这意味着它会保持CPU的活跃状态,而不是使线程进入休眠。
含义:自旋锁是一种在等待解锁时使线程保持忙等(busy-wait)的锁,这意味着线程会持续占用CPU时间直到它能获取到锁。
使用场景:自旋锁适用于锁持有时间非常短且线程不希望在操作系统调度中频繁上下文切换的场景。这通常用在低延迟系统中,或者当线程数量不多于CPU核心数量时,确保CPU不会在等待锁时空闲。
五、唯一锁(Unique Lock)
std::unique_lock
含义: 比std::lock_guard更灵活的锁,支持延迟锁定、时间锁定、以及在同一作用域中的锁的转移。
使用场景: 需要在复杂控制流中灵活管理锁的情况。
六、锁保护(Lock Guard)
std::lock_guard
含义: 自动管理锁的生命周期,确保作用域结束时释放锁。
使用场景: 当你需要确保在当前作用域结束时自动释放锁,以避免死锁。
条件变量std::condition_variable
条件变量是一种同步原语,它可以阻塞一个或多个线程,直到某个特定条件为真。条件变量总是与互斥锁(std::mutex)一起使用,以避免竞争条件。基本操作包括:
等待(wait):线程阻塞,并释放其持有的互斥锁,直到另一个线程通知(notify)条件变量。
通知(notify_one/notify_all):解除一个或所有等待线程的阻塞状态。
如何使用条件变量
条件变量用于复杂的同步问题,例如当线程需要等待某些条件(如资源可用或任务完成)满足时。它们不仅用于避免死锁,还用于减少不必要的忙等待,使得线程管理更为高效。
死锁避免方法
避免嵌套锁
unique_lock<recursive_mutex> lck(mtx)
加锁时序
确保所有线程以相同的顺序获取锁,这样可以避免循环等待的条件,从而预防死锁的发生
加锁时限
通过尝试加锁的超时机制,如果一定时间内无法获取所有需要的锁,则放弃已经获取的锁,并重新尝试。mutex.try_lock_for(_std::chrono::milliseconds(500))
死锁检测
检测的原理采用另一个线程定时对图进程检测是否有环的存在
使用条件变量
条件变量通常用于阻塞一个或多个线程直到某个特定条件为真。通过使用条件变量,可以避免不必要的锁占用和复杂的锁管理逻辑,从而降低死锁的风险。
最小化锁持有时间
加{}确保线程持有锁的时间尽可能短,这可以减少死锁发生的机会。线程应当仅在访问共享资源时持有锁,并在不需要时立即释放锁。
使用同步机制
可以使用无锁编程技术或利用并发库中提供的更高级的同步原语,如std::atomic,这些机制在设计时考虑了避免死锁。
死锁定位方法
代码日志
业务逻辑看日志到哪一块停止日志打印,分析附近代码逻辑,查看是否有加锁相关信息
,锁是否使用方式和逻辑正确。
通过coredump文件分析各个线程锁信息
g++ --std=c++11 -g -O0 [xxx].cpp -o [xxx] // -O0 表示不做优化 -O1 为默认优化
运行【xxx】,查看进程pid
ps -ef|grep [xxx]
kill -3 [pid] //产生coredump文件
gdp查看分析各个线程锁信息
gdb -c ./core-[pid] ./xxx
bt //调用栈
info thread //线程可查看锁信息
thread 2
bt
f 7
p mtx1
thread 3
bt
f 7
p mtx2