一个简单有效的即时检测线程死锁的方法(附c++源代码)(原创)

文章原创,如若转载,请标明出处https://blog.csdn.net/liaozhilong88/article/details/80354414)。

通常来说,死锁就是线程之间发生锁资源的抢夺,比方说:线程1拥有了锁A未释放而还想去拥锁B,而线程2拥有了锁B未释放却还想去拥有锁A,于是乎他们互相等待,谁都获取不到新锁资源。如下图:

            已经拥有的锁     还想拥有的锁

线程1              A               B

线程2              B               A

当然上述情况是最简单的死锁,通常死锁还可能在多个线程发生,他们之间相互抢夺,各不相让。如下图:

            已经拥有的锁     还想拥有的锁

线程1              A               C

线程2              B               A

线程3              C              B

以上是三个线程的锁资源抢夺,当更多线程加入锁资源抢夺的时候,这种情况会更加的复杂。以下是相应死锁检测思路:

  • 首先这些锁占用情况和锁需求情况我们可以记录起来,即每个锁被哪个线程所占用,我们把这个线程的TID记录下来,以下代码是一个普通锁的类封装:
class Mutex
{
public:
	inline Mutex():m_dwOwnedThreadId(0)	{
		::InitializeCriticalSection(&m_sesion);
	}
	~Mutex(){
		::DeleteCriticalSection(&m_sesion);
	}

public:
    void lock(void) const
    {
        ::EnterCriticalSection(&m_sesion);
        m_dwOwnedThreadId = GetCurrentThreadId(); //当外部调用lock的时候  记录当前的锁资源是被哪个线程所拥有的
    }

    void unlock(void) const
    {
        ::LeaveCriticalSection(&m_sesion); 
        m_dwOwnedThreadId = 0;  //线程释放了锁资源 m_dwOwnedThreadId为0表示当前锁未被占用
    }
private:
    mutable CRITICAL_SECTION	m_sesion;
    mutable DWORD m_dwOwnedThreadId;   //当前锁被哪个线程所占用,记录相应的线程ID, 一个锁对象只能被一个线程占用
};
  • 好了,当发生死锁的情况下,外部线程调用lock()函数去获取锁资源的时候,该锁一定是被其他线程锁占用,即成员变量m_dwOwnedThreadId不为零,这时候用一个全局的map变量来记录当前线程的线程ID以及目标锁被其他线程锁占用的线程ID,这样就简单记录了当前线程ID以及当前线程需要获取已经被占用锁的线程ID之间的映射关系,就像上图的层次关系,代码如下:
std::map<DWORD, DWORD> g_mapCurTidWantOwnResTid;  //定义全局变量,记录当前等待锁资源的线程ID,以及对应这个锁已经被其他线程占用的线程ID 形成相应的映射关系

class Mutex
{
public:
	inline Mutex():m_dwOwnedThreadId(0)	{
		::InitializeCriticalSection(&m_sesion);
	}
	~Mutex(){
		::DeleteCriticalSection(&m_sesion);
	}

public:
    void lock(void) const
    {
         if(0 != m_dwOwnedThreadId && m_dwOwnedThreadId != GetCurrentThreadId()) //锁已经被占用,需要等待而无法进入了     如果锁被当前线程占用,那么依旧可以重入(递归锁),也就是一个线程可以多次调用lock()函数而不会阻塞
         {
             cout<<"当前线程:"<<GetCurrentThreadId()<<"   想获取已经被线程:"<<m_dwOwnedThreadId<<" 占用的锁:"<<endl;
             g_mapCurTidWantOwnResTid[GetCurrentThreadId()] = m_dwOwnedThreadId;  //记录资源抢夺对应的关系
         }
         ::EnterCriticalSection(&m_sesion);
         m_dwOwnedThreadId = GetCurrentThreadId();//当前线程拥有了锁资源
         g_mapCurTidWantOwnResTid.erase(m_dwOwnedThreadId);//已经成功获取了锁 如果有映射关系解除相应的映射关系
    }
    void unlock(void) const
    {
         ::LeaveCriticalSection(&m_sesion); 
         m_dwOwnedThreadId = 0; //当前线程释放了锁资源
    }
 ..............

三个线程运行时,发生死锁状态的三个线程代码如下:  

DWORD WINAPI  DeadLockThread1( PVOID pVoid )
{
        mutexA.lock();
        cout<<" mutexA.locked "<<endl;
        Sleep(3500);
        mutexC.lock();  //线程1,拥有锁A,还想拥有锁C
}
DWORD WINAPI  DeadLockThread2( PVOID pVoid )
{
        mutexB.lock();
        cout<<" mutexB.locked "<<endl;
        Sleep(3000);
        mutexA.lock(); //线程2,拥有锁B,还想拥有锁A
}
DWORD WINAPI  DeadLockThread3( PVOID pVoid )
{
       mutexC.lock();
       cout<<" mutexC.locked "<<endl;
       Sleep(2000);
       mutexB.lock();  //线程3,拥有锁C,还想拥有锁B
}

三个线程锁的抢夺示意图:

  

运行代码,输出如下:

同样,如果两个线程锁的抢夺示意图:

运行代码,输出如下:

接下来关键要看上图中当前已经拥有锁和想拥有已经被占用的锁的相互关系了:

1、线程1拥有锁A,想拥有锁C

2、锁C被线程3占用,但线程3想拥有锁B

3、锁B被线程2占用,但线程2想拥有锁A

4、锁A被线程1占用

根据上面的相互关系我们可以看出来,如果形成死锁,那么至少会形成一个锁资源引用的闭环,否则认为没有发生死锁。即从A出发,最后还能回到A;同样两个线程死锁的相互关系如下:

这种关系如果表现在线程ID上的对应关系也是很明显的:

于是乎,我们可以在lock()函数等待使用锁的过程中即可判断当前锁资源访问有没有形成锁资源抢夺的闭环以此来判断死锁 

void lock(void) const
{
       if(0 != m_dwOwnedThreadId && m_dwOwnedThreadId != GetCurrentThreadId()) //锁已经被占用,需要等待而无法进入了     如果锁被当前线程占用,那么依旧可以重入(递归锁),也就是一个线程可以多次调用lock()函数而不会阻塞
       {
              cout<<"当前线程:"<<GetCurrentThreadId()<<" 想获取已经被线程:"<<m_dwOwnedThreadId<<" 占用的锁"<<endl;
             g_mapCurTidWantOwnResTid[GetCurrentThreadId()] = m_dwOwnedThreadId; 

            std::map<DWORD, DWORD> mapCurTidWantOwnResTidTmp = g_mapCurTidWantOwnResTid;
            if(RecurFindIsDeadlocked(GetCurrentThreadId(), GetCurrentThreadId(), m_dwOwnedThreadId, mapCurTidWantOwnResTidTmp)) //这里递归查找是否形成了锁资源抢夺的闭环
            {
                    cout<<"当前线程:"<<GetCurrentThreadId()<< " 发生了死锁!"<<endl;
            }
      }
     ::EnterCriticalSection(&m_sesion);
     m_dwOwnedThreadId = GetCurrentThreadId();
     g_mapCurTidWantOwnResTid.erase(m_dwOwnedThreadId);//已经成功获取了锁 如果有映射关系解除相应的映射关系
}
 
bool RecurFindIsDeadlocked(DWORD dwFirstKey, DWORD dwKey, DWORD dwVal, std::map<DWORD, DWORD>& mapCurTidWantOwnResTid) //递归查找
{
       bool bRet = false;
       for (std::map<DWORD, DWORD>::iterator it = mapCurTidWantOwnResTid.begin(); it != mapCurTidWantOwnResTid.end(); it++) //把自己当前关系移除 防止进入递归死循环
       {
              if(dwKey == it->first && dwVal == it->second)
              {
                    mapCurTidWantOwnResTid.erase(it);
                    break;
             }
       }
       for (std::map<DWORD, DWORD>::iterator it = mapCurTidWantOwnResTid.begin(); it != mapCurTidWantOwnResTid.end(); it++)
       {
              if(it->first == dwVal) //当前键值关系是当前的  key== 前一层关系的value
              {
                     if(dwFirstKey == it->second)  //当前键值关系的value 就是最开始自己想找的    A出发最后总算找到了A
                     {
                            return true;
                    }
                   return RecurFindIsDeadlocked(dwFirstKey, it->first, it->second, mapCurTidWantOwnResTid);   //继续下一层的查找
             }
      }
      return bRet;
}

好,验证下:

两个线程发生死锁状态输出:

三个线程发生死锁状态:

注意:只有在最后一个线程加入到锁抢夺的时候才形成了闭环,才真正形成了死锁。

好,这个时候如果发现了死锁,我们要怎么判断哪里出了问题呢。

1.如果在调试机器出现,可以用IDE直接attach到这个进程,然后下断点,查看堆栈

2.使用外部工具。

首先打开大名鼎鼎的Process Explorer(procexp.exe)工具:

然后找到对应的进程:DeadLock.exe,然后双击进程名称会跳转到进程属性页:

然后我们在选中一个死锁的线程,双击选中的项,就可以看到这个线程的调用堆栈了:

话题的拓展

其实死锁不止发生在像上面的线程间锁的竞争上,还包括线程间的相互等待上,看下面的例子:

DWORD WINAPI WaitDeadLockThread1(LPVOID lpParameter)
{
	cout<<"WaitDeadLockThread1, and want lock mutexA"<<endl;
	mutexA.lock();   //想获取已经被主线程占用的锁
	return 0;
}

int _tmain(int argc, _TCHAR* argv[])
{
	mutexA.lock();
	cout<<"main thread mutexA.locked "<<endl;
	DWORD dwTid1;	
	HANDLE hThread1 = CreateThread(NULL, 0, WaitDeadLockThread1, 0, 0, &dwTid1);
        cout<<"当前线程:"<<GetCurrentThreadId()<<" 正在等待线程:"<<dwTid1<<" 执行完成!"<<endl;
	WaitForSingleObject(hThread1, INFINITE);   //等待线程WaitDeadLockThread1的执行完成,但是上面这个线程其实已经死锁了
        cout<<"main thread wait finish"<<endl;
}

像上面的情况,发生死锁的原因并不是因为在等待一个闭环的锁而导致的了,而是因为其中一个线程等待另一个想得到锁的线程而导致的。执行后输出的结果如下:

解决方案:

对于这种问题其实也是相类似的处理,只需要在主线程执行WaitForSingleObject之前把当前线程和需要等待的目标线程ID关系都记录起来,当等待结束后移除这个等待关系即可:

int _tmain(int argc, _TCHAR* argv[])
{
      mutexA.lock();
      cout<<"main thread mutexA.locked "<<endl;
      DWORD dwTid1; 
      HANDLE hThread1 = CreateThread(NULL, 0, WaitDeadLockThread1, 0, 0, &dwTid1);
      g_mapCurTidWantOwnResTid[GetCurrentThreadId()] = dwTid1; //把自己正在等待的线程ID和等待的目标线程ID都记录起来
      cout<<"当前线程:"<<GetCurrentThreadId()<<" 正在等待线程:"<<dwTid1<<" 执行完成!"<<endl;
      WaitForSingleObject(hThread1, INFINITE);
      g_mapCurTidWantOwnResTid.erase(GetCurrentThreadId());
     
      cout<<"main thread wait finish"<<endl;
}

好,我们再试试,运行程序:

代码下载地址:点击下载  (windows版)    下载链接2  (windows版) 下载链接3(linux版)

(标注:Demo里的代码并没有对全局变量g_mapCurTidWantOwnResTid做同步保护,请自行另外对变量做加锁保护)

引申阅读:上文的方法仅仅适用于有源代码并调用自己的封装锁的情况下,但当一个程序的部分源代码(比如Sdk)不在自己手里,也就是有些代码并没有调用直接的封装锁的情况下并不能实时的检测到死锁。这种情况可以阅读下文《检测线程死锁的另一种方法

发布了7 篇原创文章 · 获赞 1 · 访问量 6987

猜你喜欢

转载自blog.csdn.net/liaozhilong88/article/details/80354414
今日推荐