系列服务器开发
前言
死锁问题被认为是线程/进程间切换消耗系统性能的一种极端情况。在死锁时,线程/进程间相互等待资源,而又不释放自身的资源,导致无穷无尽的等待,其结果是任务永远无法执行完成。
一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程都处于阻塞状态,无法继续。这样会出现一种操作“无反应”的结果。
死锁产生的4个必要条件?
产生死锁的必要条件:
互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。
死锁避免方法:
1、加锁时序
2、加锁时限
3、死锁检测
一、常见死锁案例?
几种常见的死锁案例:
同一个线程调用多次锁,导致了死锁。B在等待A释放锁,A在等待B加锁后释放
{
void A(){
_mutex.lock();
B();
_mutex.unlock();
return;
}
void B(){
_mutex.lock();
mutex.unlock();
return;
}
private:
int a = 0;
std::mutex _mutex;
}
同一个线程return少了释放锁,再次加锁的时候就会永远获取不到锁。因此加锁与解锁必须承对出现
{
std::mutex _mutex;
void A()
{
_mutex.lock();
if(.....){
return;
}
_mutex.unlock();
return;
}
}
线程 1 先获取互斥锁 1、然后互斥锁2,线程2尝试去获取互斥锁2、然后互斥锁1,然后就造成了相互等待,永久阻塞的情况,即死锁。因此应该以相同的顺序进行加锁。
{
public:
void A()
{
_mutex.lock();
_mutex2.lock();
//共享资源
_mutex2.unlock();
_mutex.unlock();
return;
}
void B()
{
_mutex2.lock();
_mutex.lock();
//共享资源
_mutex.unlock();
_mutex2.unlock();
return;
}
private:
int s = 0;
std::mutex _mutex;
std::mutex _mutex2;
}
多个线程申请锁的顺序形成相互依赖的环形
A->B->C->D->A,当A等待B释放锁,B等待C释放锁,C等待D释放锁,D又等待A释放锁,导致了死锁。
几种常见的方式用来解决死锁问题:
死锁是不应该在程序中出现的,在编写程序时应该尽量避免出现死锁。
1、避免多次锁定。尽量避免同一个线程进行多次Lock 。
2、使用相同的加锁顺序。如果多个线程需要对多个 Lock 进行锁定,则应该保证它们以相同的顺序请求加锁。
3、使用定时锁。程序在调用 wait() 方法加锁时可指定 timeout 参数,该参数指定超过 timeout 秒后会自动释放对 Lock 的锁定,这样就可以解开死锁了。
4、死锁检测。死锁检测是一种依靠算法机制来实现的死锁预防机制,它主要是针对那些不可能实现按序加锁,也不能使用定时锁的场景的。
二、避免死锁或者死锁定位方法:
tips:对mutex解锁永远不会抛出异常,反之对其加锁可能会抛出异常
1、常见死锁与避免死锁的方法
1、使用C++ RAII,避免忘记解锁造成的死锁
unique_lock<mutex>
lock_guard<mutext>
2、使用c++ std::lock锁,来避免多次加锁顺序导致的死锁
可以一次锁住两个或者两个以上的互斥量。(最少锁两个)、它不存在这种因为多个线程中因为锁的顺序问题导致死锁的风险问题。adopt_lock假设调用方线程已拥有互斥的所有权
或者:
使用C++17标准的std::scope_lock模板类,可以同时锁定多个互斥量,并且能够在析构时自动解锁互斥量;
class MyFun
{
public:
void fun1(){
while (1){
// 死锁
// lock_guard<mutex> a(m1);
// lock_guard<mutex> b(m2);
// lock(m1,m2);
// 避免死锁
lock(m1,m2); // 同时锁住
lock_guard<mutex> lock_a(m1,adopt_lock);// 只是拥有锁的所有权,但是不会再重复上锁
lock_guard<mutex> lock_b(m2,adopt_lock);
//等价于
//std::unique_lock<std::mutex> lock_a(m1, defer_lock);
//std::unique_lock<std::mutex> lock_b(m2, defer_lock);
//lock(m1,m2);
//等价于
//std::scoped_lock lock(m1, m2); // C++17
cout << "this is fun 1 " << endl;
}
}
void fun2(){
while (1){
// 死锁代码
// lock_guard<mutex> b(m2);
// lock_guard<mutex> a(m1);
// lock(m1,m2);
// 避免死锁
lock(m1,m2);
lock_guard<mutex> lock_a(m1,adopt_lock);
lock_guard<mutex> lock_b(m2,adopt_lock);
//等价于
//std::unique_lock<std::mutex> lock_a(m1, defer_lock);
//std::unique_lock<std::mutex> lock_b(m2, defer_lock);
//lock(m1,m2);
//等价于
//std::scoped_lock lock(m1, m2); // C++17
cout << "this is fun 2 " << endl;
}
}
private:
std::mutex m1;
std::mutex m2;
}
3、如果单线程对同一个锁加锁两次,那么这会导致死锁,递归锁可以解决此问题。
递归锁会记录上一次对自己加锁的线程id,如果发现是当前执行线程和上一次加锁线程是同一个则不再做加锁动作。
class MyFun
{
public:
void fun(){
lock_guard<mutex> lock(_mutex);// 只是拥有锁的所有权,但是不会再重复上锁
cout << "this is fun " << endl;
}
private:
std::recursive_mutex _mutex;
}
4、两个线程相互wait等待对方将f1,f2置位,导致的死锁,通常使用wait时,需要notify、notify_all来解锁或者通过wait_for等设定超时时间来避免死锁。
class MutexTest{
public:
void a()
{
std::unique_lock<std::mutex> locker(_mutex);
std::_condition.wait(locker,[]{ return f1;});
std::cout<< "a" << std:endl;
f2=true;
f1=false;
}
void b()
{
std::unique_lock<std::mutex> locker(_mutex);
_condition.wait(locker, [] { return f2; });
std::cout<<"b"<< std:endl;
f1 = true;
f2=false;
}
private:
std::mutex _mutex;
std::condition_variable _condition;
bool f1=true,f2=true;
};
2、定位死锁方法
1、log打印法,通过下面实现,可以通过成对打印判定哪个线程调用出现了锁不对称的用法。
class PrintMutex: public std::mutex{
public:
void lock(){
int count=++_count;
butil::debug::StackTrace stacktrace;
std::cout<<"lock pid:"<<std::this_thread::get_id()<<" name="<<_lockName<<" count="<<count <<std::endl;
std::cout<<stacktrace.ToString()<<std::endl;
std::mutex::lock();
std::cout<<"lock pid:"<<std::this_thread::get_id()<<" name="<<_lockName<<" count="<<count<<std::endl;
}
bool try_lock() noexcept{
bool ret=std::mutex::try_lock();
if(ret){
butil::debug::StackTrace stacktrace;
std::cout<<"try_lock pid:"<<std::this_thread::get_id()<<" name="<< _lockName <<std::endl;
std::cout << stacktrace.ToString() <<std::endl;
}
return ret;
}
void unlock(){
butil::debug::StackTrace stacktrace;
std::cout << "unlock pid:"<<std::this_thread::get_id()<<" name="<< _lockName <<std::endl;
std::cout << stacktrace.ToString() <<std::endl;
std::mutex::unlock();
}
private:
PrintMutex(const char* __lockName):std::mutex(),
_lockName(__lockName){
std::cout<< lockName<< " create instance" <<std::endl;
}
~PrintMutex(){
std::cout<< _lockName<< " quit instance" <<std::endl;
}
std::string _lockName;
static std::atomic_int _count={0};
};
2、通过线程堆栈定位
通过pstack,打印堆栈信息,通过多次pstack堆栈信息,发现某些线程阻塞到同一函数,即大概率为死锁现象。
#include <mutex>
#include <memory>
class MUTEXTEST
{
public:
void mutexCall(){
std::lock_guard<std::mutex> lock(_mutex);
mutexFun2();
}
void mutexFun2()
{
std::lock_guard<std::mutex> lock(_mutex);
_value++;
}
private:
int _value = 0;
std::mutex _mutex;
};
int main(int argc, char* argv[]) {
std::cout<< "will go to mutex begin" <<std::endl;
MUTEXTEST mutex_instance;
mutex_instance.mutexCall();
std::cout<< "will go to mutex finished" <<std::endl;
return 0;
}
获取进行pid
ps -elf|grep vtest
打印堆栈信息
pstack 14270
通过多次堆栈打印,发现线程阻塞到了mutexFun2的调用上,重点分析mutexCall->mutexFun2的调用逻辑。
总结
本文主要讲解死锁的常见情况,避免死锁的方法、与定位死锁的方法,其中最重要的是,第一时间保存堆栈现场,是分析死锁现象的重要条件。