searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

一个即时检测线程死锁的方案

2023-03-31 08:07:16
17
0

通常来说,死锁就是线程之间发生锁资源的抢夺,比方说:线程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;
}

 

好,验证下:

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

三个线程发生死锁状态:

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

 

 

 

 

 

 

0条评论
0 / 1000