Click here to Skip to main content
15,867,453 members
Articles / Desktop Programming / MFC
Article

Method for detecting and solving deadlocks in multithreading applications

Rate me:
Please Sign up or sign in to vote.
4.63/5 (5 votes)
17 Oct 20013 min read 90.2K   1.2K   31   7
Presenting a method for detecting and solving deadlocks in multithreading applications using critical sections as synchronization objects

Introduction

In this article we are presenting a method for solving thread deadlock problems. We applied this method in a real life multithreading project. We are presenting the solution we gave to the deadlock problem when using critical sections as synchronization objects, but the presented ideas can be easily extended for other synchronization objects. The main idea of the method is to develop our own critical section class derived from MFC CCriticalSection class, and inside this class to implement a static container in which to keep state information for every created instance object. In the overridden functions Lock() and Unlock() the static container is accessed and the state information pertaining to the current object is updated. The information in the static container can be saved in a file from a special thread. This special thread is put to wait for a system event to be set in order to save the state information and to unlock all the critical section objects in the container. The system event is set externally to the application from a distinct small console application.

Implementation

In order to make the work with worker threads easier and more object oriented we developed our own worker thread class in which we wrapped some threading Windows API functions:

class CWThread
{
public:
  //Constructor
  CWThread() : m_hThread(NULL), m_bSuspended(TRUE) {}

  //Destructor
  virtual ~CWThread()
  {
    Stop();
  }

  //Create
  BOOL Create(LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter=NULL);

  //Resume the thread
  BOOL Resume();

  //Start the thread
  BOOL Start()
  {
    //Technically is the same as Resume()
    return Resume();
  }

  //Suspend the thread
  BOOL Suspend();

  //Get thread priority
  int GetPriority()
  {
    return GetThreadPriority(m_hThread);
  }

  //Set thread priority
  BOOL SetPriority(int iPriority)
  {
    return (TRUE == SetThreadPriority(m_hThread, iPriority));
  }

  //Stop the thread
  BOOL Stop();

  //Check if is created
  BOOL IsCreated()
  {
    return (m_hThread != NULL);
  }

  //Check if is suspended
  BOOL IsSuspended()
  {
    return m_bSuspended;
  }

private:
  CWThread& operator=(const CWThread&); //disallow copy
  //Handle to the thread
  HANDLE m_hThread;
  //Suspension Flag
  BOOL m_bSuspended;
};

//Create
BOOL CWThread::Create(LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter)
{
  if(NULL == m_hThread)
  {
    DWORD dwThreadID;
    //Always created in a Suspended State
    m_hThread = CreateThread((LPSECURITY_ATTRIBUTES)NULL, (DWORD)0, lpStartAddress,
      lpParameter, (DWORD)CREATE_SUSPENDED, (LPDWORD)&dwThreadID);
    if(m_hThread != NULL)
    {
      //Initialized to Normal Priority
      SetThreadPriority(m_hThread, THREAD_PRIORITY_NORMAL);
      return FALSE; //OK
    }
  }
  return TRUE; //ERROR
}

//Resume the thread
BOOL CWThread::Resume()
{
  if((m_hThread!=NULL)&&(TRUE==m_bSuspended))
  {
    if(ResumeThread(m_hThread)!=0xFFFFFFFF)
    {
      m_bSuspended = FALSE;
      return FALSE; //OK
    }
  }
  return TRUE; //ERROR
}

//Suspend the thread
BOOL CWThread::Suspend()
{
  if((m_hThread!=NULL)&&(FALSE==m_bSuspended))
  {
    //Set the Flag before suspending (otherwise is not working)
    m_bSuspended = TRUE;
    if(SuspendThread(m_hThread)!=0xFFFFFFFF)
      return FALSE; //OK
    //If not successfull Reset the flag
    m_bSuspended = FALSE;
  }
  return TRUE; //ERROR
}

//Stop the thread
BOOL CWThread::Stop()
{
  if(m_hThread != NULL)
  {
    TerminateThread(m_hThread, 1);
    //Closing the Thread Handle
    CloseHandle(m_hThread);
    m_hThread = NULL;
    m_bSuspended = TRUE;
    return FALSE; //OK
  }
  return TRUE; //ERROR
}

In our critical section class we are using as a container an STL map in which we are mapping a unique instance IDs to a data structure. In the data structure we keep some information string and a pointer to the object instance. The pre-processor flag __INFOCRITSECT_DEBUG__ is used to activate the state updating operations in the Lock() and Unlock() functions.

Our critical section class:

#define __INFOCRITSECT_DEBUG__

#pragma warning(disable:4786)

#include "Afxmt.h"
#include "WThread.h"
#include <string>
#include <strstream>
#include <fstream>
#include <map>

using namespace std;

struct SData
{
  //CONSTRUCTOR
  SData(string const& rostrBeforeLock="No Init", string const& rostrAfterLock="No Init",
    string const& rostrBeforeUnlock="No Init", string const& rostrAfterUnlock="No Init",
    string const& rostrDesc="No Desc") : m_ostrBeforeLock(rostrBeforeLock), 
    m_ostrAfterLock(rostrAfterLock), m_ostrBeforeUnlock(rostrBeforeUnlock), 
    m_ostrAfterUnlock(rostrAfterUnlock), m_ostrDesc(rostrDesc),
    m_poCriticalSection(NULL) {}
  string m_ostrBeforeLock;
  string m_ostrAfterLock;
  string m_ostrBeforeUnlock;
  string m_ostrAfterUnlock;
  string m_ostrDesc;
  CCriticalSection* m_poCriticalSection;
};

//CInfoCritSect class - Implementing the Singleton Design Pattern
class CInfoCritSect : public CCriticalSection
{
public:
  //CONSTRUCTOR
  CInfoCritSect(string const& rostrDesc="No Desc")
  {
    //Add a new data element in the map
    sm_oCritSect.Lock();
      m_iIndex = sm_iCount;
      sm_iCount++;
      sm_oMap[m_iIndex] = SData();
      sm_oMap[m_iIndex].m_ostrDesc = rostrDesc;
      sm_oMap[m_iIndex].m_poCriticalSection = this;
    sm_oCritSect.Unlock();
  }

  void SetDescription(string const& rostrDesc)
  {
    //Remove the data element from the map
    map<unsigned int, SData>::iterator it;
    sm_oCritSect.Lock();
      it = sm_oMap.find(m_iIndex);
      if(it != sm_oMap.end())
        it->second.m_ostrDesc = rostrDesc;
    sm_oCritSect.Unlock();
  }

  //DESTRUCTOR
  ~CInfoCritSect()
  {
    //Remove the data element from the map
    map<unsigned int, SData>::iterator it;
    sm_oCritSect.Lock();
      it = sm_oMap.find(m_iIndex);
      if(it != sm_oMap.end())
        sm_oMap.erase(it);
    sm_oCritSect.Unlock();
  }
	
  void Lock(unsigned int uiLine=0, string const& rostrFileName="");

  void Unlock(unsigned int uiLine=0, string const& rostrFileName="");

  void BeforeLock(unsigned int uiLine, string const& rostrFileName)
  {
    //Find the position in map
    map<unsigned int, SData>::iterator it;
    sm_oCritSect.Lock();
    it = sm_oMap.find(m_iIndex);
    if(it != sm_oMap.end())
    {
      DWORD dwId = ::GetCurrentThreadId();
      ostrstream ostr;
      ostr << "Trying to Lock: ID=" << ::GetCurrentThreadId()
        << ", File=" << rostrFileName << ", Line=" << uiLine 
        << ", Time=" << ::GetTickCount();
      ostr << ends;
      it->second.m_ostrBeforeLock = ostr.str();
      ostr.freeze(0);
    }
    sm_oCritSect.Unlock();
  }

  void AfterLock(unsigned int uiLine, string const& rostrFileName)
  {
    //Find the position in map
    map<unsigned int, SData>::iterator it;
    sm_oCritSect.Lock();
    it = sm_oMap.find(m_iIndex);
    if(it != sm_oMap.end())
    {
      DWORD dwId = ::GetCurrentThreadId();
      ostrstream ostr;
      ostr << "Locked: ID=" << ::GetCurrentThreadId()
        << ", File=" << rostrFileName << ", Line=" << uiLine 
        << ", Time=" << ::GetTickCount();
      ostr << ends;
      it->second.m_ostrAfterLock = ostr.str();
      ostr.freeze(0);
    }
    sm_oCritSect.Unlock();
  }

  void BeforeUnlock(unsigned int uiLine, string const& rostrFileName)
  {
    //Find the position in map
    map<unsigned int, SData>::iterator it;
    sm_oCritSect.Lock();
    it = sm_oMap.find(m_iIndex);
    if(it != sm_oMap.end())
    {
      DWORD dwId = ::GetCurrentThreadId();
      ostrstream ostr;
      ostr << "Trying to Unlock: ID=" << ::GetCurrentThreadId()
        << ", File=" << rostrFileName << ", Line=" << uiLine 
        << ", Time=" << ::GetTickCount();
      ostr << ends;
      it->second.m_ostrBeforeUnlock = ostr.str();
      ostr.freeze(0);
    }
    sm_oCritSect.Unlock();
  }

  void AfterUnlock(unsigned int uiLine, string const& rostrFileName)
  {
    //Find the position in map
    map<unsigned int, SData>::iterator it;
    sm_oCritSect.Lock();
      it = sm_oMap.find(m_iIndex);
      if(it != sm_oMap.end())
      {
        DWORD dwId = ::GetCurrentThreadId();
        ostrstream ostr;
        ostr << "Unlocked: ID=" << ::GetCurrentThreadId()
          << ", File=" << rostrFileName << ", Line=" << uiLine 
          << ", Time=" << ::GetTickCount();
        ostr << ends;
        it->second.m_ostrAfterUnlock = ostr.str();
        ostr.freeze(0);
      }
    sm_oCritSect.Unlock();
  }

  static void PrintInfo()
  {
    //Open Output File
    ofstream fout("Deadlocks.out");
    if(!fout)
      return;
    sm_oCritSect.Lock();
      map<unsigned int, SData>::iterator it = sm_oMap.begin();
      while(it != sm_oMap.end())
      {			
        fout << "Critical Section: " << it->second.m_ostrDesc << endl;
        fout << " " << it->second.m_ostrBeforeLock << endl;
        fout << " " << it->second.m_ostrAfterLock << endl;
        fout << " " << it->second.m_ostrBeforeUnlock << endl;
        fout << " " << it->second.m_ostrAfterUnlock << endl;
        fout << endl;
        it++;
      }
    sm_oCritSect.Unlock();
    fout.close();
  }

  static void UnlockAll()
  {
    sm_oCritSect.Lock();
      map<unsigned int, SData>::iterator it = sm_oMap.begin();
      while(it != sm_oMap.end())
      {
        it->second.m_poCriticalSection->Unlock();
        it++;
      }
    sm_oCritSect.Unlock();
  }

private:
  static CCriticalSection sm_oCritSect;
  static map<unsigned int, SData> sm_oMap;
  static unsigned int sm_iCount;
  unsigned int m_iIndex;
};

inline void CInfoCritSect::Lock(unsigned int uiLine, string const& rostrFileName)
{
#ifdef __INFOCRITSECT_DEBUG__
  BeforeLock(uiLine, rostrFileName);
#endif
  CCriticalSection::Lock();
#ifdef __INFOCRITSECT_DEBUG__
  AfterLock(uiLine, rostrFileName);
#endif
}

inline void CInfoCritSect::Unlock(unsigned int uiLine, string const& rostrFileName)
{
#ifdef __INFOCRITSECT_DEBUG__
  BeforeUnlock(uiLine, rostrFileName);
#endif
  CCriticalSection::Unlock();
#ifdef __INFOCRITSECT_DEBUG__
  AfterUnlock(uiLine, rostrFileName);
#endif
}

unsigned int CInfoCritSect::sm_iCount = 0;

map<unsigned int, SData> CInfoCritSect::sm_oMap;

CCriticalSection CInfoCritSect::sm_oCritSect;

The special unlocking thread is derived from our general worker thread class we presented above and implements the Singleton design pattern:

class CDeadlocksThread : public CWThread
{
private:
  //CONSTRUCTOR - Private to prevent Construction from outside
  CDeadlocksThread();
	
public:
  //Create
  bool Create();
  //DESTRUCTOR
  ~CDeadlocksThread();
  //Getting the address of the unique instance
  static CDeadlocksThread* GetInstance();
	
private:
};

#include "stdafx.h"
#include "DeadlocksThread.h"
#include "InfoCritSect.h"

//Thread Function
UINT DeadlocksThreadProc(LPVOID pParam);

//CONSTRUCTOR
CDeadlocksThread::CDeadlocksThread()
{
}

//DESTRUCTOR
CDeadlocksThread::~CDeadlocksThread()
{
}

//Getting the address of the unique instance
CDeadlocksThread* CDeadlocksThread::GetInstance()
{
  static CDeadlocksThread soDeadlocksThread;
  return &soDeadlocksThread;
}

//Create
bool CDeadlocksThread::Create()
{
  if(TRUE == CWThread::Create((LPTHREAD_START_ROUTINE)DeadlocksThreadProc))
    return true;
  else
    return false;
}

//The Thread Function
UINT DeadlocksThreadProc(LPVOID pParam)
{
  HANDLE hEv = ::CreateEvent(NULL, FALSE, FALSE, "DeadlockDetection");
  while(true)
  {
    ::WaitForSingleObject(hEv, INFINITE);
    CInfoCritSect::PrintInfo();
    CInfoCritSect::UnlockAll();
  }
  return 0;
}

In DeadlocksThreadProc() when the system event is set the static functions PrintInfo() and UnlockAll() are called for saving the state information in a file and respectively unlock all the instance objects. The console application used for fireing the event is very simple:

#include "windows.h"
#include <iostream>

using namespace std;

void main(int argc, char* argv[])
{
  HANDLE hEv = ::OpenEvent(EVENT_ALL_ACCESS, FALSE, "DeadlockDetection");
  if(hEv != NULL)
  {
    ::SetEvent(hEv);
    cout << "DeadlockDetection event set" << endl;
  }
  else
    cout << "Cannot open DeadlockDetection event" << endl;
  ::CloseHandle(hEv);
}

The same name is used for the system event.

In the example project TestDeadlocks.zip associated to this article is a simple dialog based MFC application. When the Start Thread button is pressed a separate thread is started in which we intentionally forget to unlock the critical section of a data reservoir after locking. Since the data reservoir is also accessed by the user interface thread in the OnPaint() handler, the effect will be that the application's user interface will be frozen. The thread is also derived from our general worker thread class:

class CThread1 : public CWThread
{
public:
  //CONSTRUCTOR
  CThread1();
  //Create
  bool Create();
  //DESTRUCTOR
  ~CThread1();
	
private:
};

//Thread Function
UINT Thread1Proc(LPVOID pParam);

//CONSTRUCTOR
CThread1::CThread1()
{
}

//DESTRUCTOR
CThread1::~CThread1()
{
}

//Create
bool CThread1::Create()
{
  if(TRUE == CWThread::Create((LPTHREAD_START_ROUTINE)Thread1Proc))
    return true;
  else
    return false;
}

//The Thread Function
UINT Thread1Proc(LPVOID pParam)
{
  CDataReservoir* poDataReservoir = CDataReservoir::GetInstance();
  poDataReservoir->Lock(__LINE__, __FILE__);
  //Forgets to Unlock()
  //poDataReservoir->Unlock(__LINE__, __FILE__);
  TRACE("\nThread1");
  return 0;
}

//Or in an Infinite Loop
/*
//The Thread Function
UINT Thread1Proc(LPVOID pParam)
{
  //Infinite loop
  while(TRUE)
  {
    CDataReservoir* poDataReservoir = CDataReservoir::GetInstance();
    poDataReservoir->Lock(__LINE__, __FILE__);
    //Forgets to Unlock()
    //poDataReservoir->Unlock(__LINE__, __FILE__);
    TRACE("\nThread1");
    Sleep(50);		
  }
  return 0;
}
*/

Our data reservoir class is not containing any real data, having only for demonstrative purposes. Specific data can be easily added in real applications. The access to this real data will be protected by the m_oInfoCritSect critical section member.

//CDataReservoir class - Implementing the Singleton Design Pattern
class CDataReservoir
{
private:
  //CONSTRUCTOR - Private to prevent Construction from outside
  CDataReservoir();
	
public:
  //DESTRUCTOR
  ~CDataReservoir();
  //Getting the address of the unique instance
  static CDataReservoir* GetInstance();
  void Lock(unsigned int uiLine=0, string const& rostrFileName="");
  void Unlock(unsigned int uiLine=0, string const& rostrFileName="");

private:
  CInfoCritSect m_oInfoCritSect;
};

inline void CDataReservoir::Lock(unsigned int uiLine, string const& rostrFileName)
{
  m_oInfoCritSect.Lock(uiLine, rostrFileName);
}

inline void CDataReservoir::Unlock(unsigned int uiLine, string const& rostrFileName)
{
  m_oInfoCritSect.Unlock(uiLine, rostrFileName);
}

//CONSTRUCTOR
CDataReservoir::CDataReservoir()
{
  m_oInfoCritSect.SetDescription("DataReservoir CS");
}

//DESTRUCTOR
CDataReservoir::~CDataReservoir()
{
}

//Getting the address of the unique instance
CDataReservoir* CDataReservoir::GetInstance()
{
  static CDataReservoir soDataReservoir;
  return &soDataReservoir;
}

The unlocking thread is created and started in the OnInitDialog() handler:

CDeadlocksThread::GetInstance()->Create();
CDeadlocksThread::GetInstance()->Start();

The freezing thread is started by pressing the Start Thread button:

void CTestDlg::OnButton1()
{
  m_oThread1.Stop();
  m_oThread1.Create();
  m_oThread1.Start();
}

We can start running the application and press the Start Thread button. After that we will see that the user interface is not updated anymore. To unlock the application we should run the FireEvent.exe console application. After that the user interface will be updated and the file Deadlocks out will be written on the disk. The information in this file is similar to:

none
Critical Section: DataReservoir CS
 Trying to Lock: ID=1104, 
    File=C:\CodeProject\Deadlocks\TestDeadlocks\TestDlg.cpp, Line=168, Time=9481373
 Locked: ID=1100, 
    File=C:\CodeProject\Deadlocks\TestDeadlocks\Thread1.cpp, Line=34, Time=9479530
 Trying to Unlock: ID=1104, 
    File=C:\CodeProject\Deadlocks\TestDeadlocks\TestDlg.cpp, Line=169, Time=9477357
 Unlocked: ID=1104, 
    File=C:\CodeProject\Deadlocks\TestDeadlocks\TestDlg.cpp, Line=169, Time=9477357

and can be easily interpreted. We can easily see that the user interface thread with ID=1104 is trying to lock the critical section but cannot do it because the critical section was locked by the worker thread with ID=1100 and after that was never unlocked, so we have a deadlock because the resource is not released by the thread with ID=1100.

We hope that you will find the information presented in this article useful!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
Romania Romania
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Generalstill good Pin
Southmountain21-Sep-20 15:02
Southmountain21-Sep-20 15:02 
Generalhelpe me Pin
marhabi18-Dec-06 11:41
marhabi18-Dec-06 11:41 
Generalgreat... Pin
stanleyyyy30-Mar-04 22:40
stanleyyyy30-Mar-04 22:40 
Great article, thank you Smile | :)
GeneralRe: great... Pin
GerB23-Feb-07 6:03
GerB23-Feb-07 6:03 
GeneralGreat idea! Pin
17-Oct-01 2:59
suss17-Oct-01 2:59 
GeneralRe: Great idea! Pin
George Anescu17-Oct-01 7:10
George Anescu17-Oct-01 7:10 
GeneralRe: Great idea! Pin
Eric Kenslow22-Oct-01 7:37
Eric Kenslow22-Oct-01 7:37 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.