Click here to Skip to main content
15,861,125 members
Articles / General Programming / Threads

Automatic Thread Synchronization - Exception Based Class Library

Rate me:
Please Sign up or sign in to vote.
3.27/5 (7 votes)
19 Oct 2020CPOL5 min read 39.9K   752   26   15
Automatic thread synchronization classes which throw exceptions instead of error codes
In this article, the topic for discussion is making automatic thread synchronization classes throw exceptions when the thread wait fails or times out. I have prepared a demo application which simulates error cases. This will help in quickly understanding the intended usage.

Introduction

Exceptions are always preferred to return codes in the object oriented world. As we all know, it will make the normal flow a fall through and separates the alternate flows (exceptional). It not only makes the normal execution flows efficient (as there is no need to check for returns) but also makes the code more clean and easy to understand. You may be thinking why I am chatty about the very well known facts. Well, I am coming to the point. How about making the automatic thread synchronization classes throw exceptions when the thread wait fails or times out? This is the topic for our discussion.

Background

This is how we normally deal with thread synchronization, see the code below:

C++
HANDLE hMutex = ::CreateMutex(…);
// other code
DWORD dwReturn = WaitForSingleObject(hMutex,dwTimeOut);
switch(dwReturn)
{
    case WAIT_OBJECT_0:
    // handle success case
    case WAIT_FAILED:
    // handle wait failed.
    case WAIT_TIMEOUT:
    // handle timeout
    case WAIT_ABANDONED:
    // handle wait abandoned
}

This is OK in the C world. But it looks bad in C++ code. We have MFC synchronization classes such as CMutex, CEvent, CSemaphore, etc. that wrap the APIs. Also, the associated classes such as CSinlgeLock and CMultiLock make the locks acquire and release automatically. But the Lock function returns either TRUE or FALSE when the wait succeeds or fails. In the case of failure, the exact return code is not available.

See the code snippet below which uses CSingleLock.

C++
CSingleLock MyLock(&m_myMutex);

if (MyLock.Lock(500))   
{
    // Lock success
    MyLock.Unlock();
} 

Now, I think we are in the context and I am proposing a solution to get rid of error codes and use exceptions instead. The synchronization classes which I offer throw exceptions when a wait failed case happens.

This will make the code cleaner as we can deal with each of the error cases such as WAIT_FAILED, WAIT_TIMEOUT etc., in separate exception handling blocks. The following table shows the error codes and their corresponding exceptions:

Error code Exception
WAIT_OBJECT_0 Normal case
WAIT_FAILED SyncObjectWaitFailed
WAIT_TIMEOUT SyncObjectWaitTimeOut
WAIT_ABANDONED SyncObjectWaitAbandoned
WAIT_ABANDONED _0 + SyncObjectWaitAbandoned

The following is the army of classes I have coded to achieve thread synchronization using an exception based strategy:

  • SynchronizerBase
  • CriticalSectionSynchronizer
  • EventSynchronizer
  • MutexSynchronizer
  • SemaphoreSynchronizer
  • SingleObjectLock
  • MultiObjectLock

The Synchronizer group of classes provide facilities for thread synchronization using CriticalSection, Event, Mutex and Semaphore. These classes can be used with automatic locking and lock releasing objects such as SingleObjectLock and MultiObjectLock. SynchronizerBase is the base class for this group of classes. These classes throw exceptions when conditions such as wait timeout arise. As we all know, an exception based mechanism avoids error code checking in the success case. It separates the normal flow and exceptional flow.

  • CriticalSectionSynchronizer - This class wraps the critical section. It serves as a facilitator for the automatic lock handling class, the SingleObjectLock. SingleObjectLock's constructor calls Lock and destructor calls UnLock.
  • MutexSynchronizer - This class wraps the mutex kernel object. It throws SyncObjectCreationFailed, SyncObjectWaitFailed, SyncObjectWaitTimedOut, SyncObjectWaitAbandoned exceptions depending on the mutex creation or return of the WaitForSingleObject function. It acts a facilitator for the automatic lock acquire and release by the SingleObjectLock and MultiObjectLock classes.
  • SemaphoreSynchronizer - This class wraps the semaphore kernel object. It throws the SyncObjectCreationFailed, SyncObjectWaitFailed, SyncObjectWaitTimedOut exceptions depending on the semaphore creation or return of the WaitForSingleObject function. It acts as a facilitator for automatic lock acquire and release by the SingleObjectLock and MultiObjectLock classes.
  • SingleObjectLock - This class handles automatic lock acquire and release by using a constructor and destructor. It ensures that the lock is properly released with the help of an UnLock call in the destructor. It works with the single object wait function, WaitForSingleObject.
  • MultiObjectLock - This class handles the automatic lock acquire and release by using a constructor and destructor. It works with multiple object wait functions such as the WaitForMultipleObjects and MsgWaitForMultipleObjects. It throws SyncObjectWaitTimedOut, SyncObjectWaitFailed, SyncObjectWaitAbandoned depending on the return value of the wait function. This object can be used to wait in a worker thread as well as a UI thread. In case a UI thread is specified, it will use MsgWaitForMultipleObjects internally. The LockHolder keeps the synchronization objects. The sync objects should be added to the LockHolder and passed to the MultiObjectLock instance. The LockHolder can be reused with the local instances of MultiObjectLock.
  • LockHolder - Holds the list of synchronization objects. It is used with MultiObjectLock. The synchronizer list is separated because we can create local instances of MultiObjectLock while the LockHolder object can be reused. So, there is no need to add the synchronizer objects again while creating another instance of MultiObjectLock.

Using the Code

Now, how to use these classes. For an example, see the code below. It shows how you can use the SingleObjectLock. The SingleObjectLock uses WaitForSingleObject whereas MultiObjectLock uses WaitForMultipleObjects and MsgWaitForMultipleObjects (if UI flag is ON).

C++
// It simulates the mutex wait timedout condition with the help
// of a child thread.
void CThreadSyncClassesDemoDlg::OnBnClickedButtonMutexTimeout()
{
    try
    {
        THREAD_INFO* pstThreadInfo = new THREAD_INFO;
        pstThreadInfo->eObjType = MUTEX;
        pstThreadInfo->csObjName = GetUniqueKernelObjName();

        // Let thread acquire the mutex
        CWinThread* pTimeoutThread = 
                    AfxBeginThread(ExceptionSimulatorThread,pstThreadInfo,0,0);
        AfxMessageBox(_T("Thread acquired lock...click OK to timeout"));
        
        // Try to acquire the mutex and get timeout
        MutexSynchronizer MtxSync(0,FALSE,pstThreadInfo->csObjName);
        SingleObjectLock Lock(&MtxSync,100);
    }
    catch (SyncObjectWaitTimeOut& WaitTimeOut)
    {
        // Your wait timeout handler
    }
    catch(SyncObjectWaitFailed& WaitFailed)
    {
        // Your wait failed handler
    }
    catch(SyncObjectWaitAbandoned& WaitAbandoned)
    {
        // Your wait abandoned handler
    }
    catch(GeneralException& GenException)
    {
        // General handler
    }
} 

The MultiObjectLock needs a helper class LockHolder which manages a container of synchronization objects. As the purpose of MultiObjectLock is to operate as a stack variable, the LockHolder object can be reused. This is particularly handy in the case of waiting in a UI thread where we have to loop around while pumping the messages. The call to MsgWaitForMultipleObjects is wrapped inside a MultiObjectLock constructor. It releases the objects that are signaled in the destructor. The HasMessageInQueue() function can be used to see if MsgWaitForMultipleObjects returned due to a message in the queue. If it returns false, that means an object is signaled and we can quit the loop.

Here is how you can use MultiObjectLock:

C++
// This will simulate the WaitForMultipleObjects timeout condition
void CThreadSyncClassesDemoDlg::OnBnClickedButtonMultiTimeout()
{
    try
    {
        CString csMutexName = GetUniqueKernelObjName();
        
        THREAD_INFO* pstThreadInfo = new THREAD_INFO;
        pstThreadInfo->eObjType = MUTEX;
        pstThreadInfo->csObjName = csMutexName;

        // Let the worker thread acquire the mutex...
        CWinThread* pTimeoutThread = 
                    AfxBeginThread(ExceptionSimulatorThread,pstThreadInfo,0,0);
        AfxMessageBox(_T
               ("Let the other thread acquire the mutex and we time out...now close"));

        // Prepare mutex and semaphore objects
        typedef std::auto_ptr<SynchronizerBase> SyncObjPtr;
        SyncObjPtr pMutexPtr(new MutexSynchronizer(0,FALSE,csMutexName));
        SyncObjPtr pSemPtr(new SemaphoreSynchronizer(0,FALSE,1,1,1,0,_T("TEST_SEMAPHORE")));
        
        // Add mutex and semaphore
        LockHolder lhLockHolderObj;
        lhLockHolderObj.AddSynchronizer(pMutexPtr.get());
        lhLockHolderObj.AddSynchronizer(pSemPtr.get());
        
        // Now, try for a WaitForMultipleObjects.
        // It will generate WaitTimeout exception.
        MultiObjectLock Lock(&lhLockHolderObj,TRUE,100,FALSE,0); 
    }
    catch (SyncObjectWaitTimeOut& WaitTimeOut)
    {
        AfxMessageBox(WaitTimeOut.GetDescription());
    }
    catch(SyncObjectWaitFailed& WaitFailed)
    {
        AfxMessageBox(WaitFailed.GetDescription());
    }
    catch(SyncObjectWaitAbandoned& WaitAbandoned)
    {
        AfxMessageBox(WaitAbandoned.GetDescription());
    }
    catch(GeneralException& GenException)
    {
        AfxMessageBox(GenException.GetDescription());
    }
} 

See below on how to use MultiObjectLock from a UI thread:

C++
// It will simulate the MsgWaitForMultipleObjects
void CThreadSyncClassesDemoDlg::OnBnClickedButtonUiMultiWait()
{
    m_ProgressDemo.SetRange(0,PROGRESS_MAX);
    
    EventSynchronizer EventSync(0,FALSE,TRUE,FALSE,0);
    typedef std::auto_ptr<SynchronizerBase> SyncObjPtr;
    SyncObjPtr pEventPtr(new EventSynchronizer(0,FALSE,TRUE,FALSE,0));
    
    CWinThread* pUIWaitThread = AfxBeginThread(UIWaitDemoThread,pEventPtr.get(),0,0);
    
    LockHolder lhLockHolderObj;
    lhLockHolderObj.AddSynchronizer(pEventPtr.get());
    for(;;)
    {
        // Call MsgWaitForMultipleObjects and allow to pass input events.
        MultiObjectLock Lock(&lhLockHolderObj,FALSE,INFINITE,TRUE,QS_ALLINPUT); 
        // If the reason to return is not a message, it is the case when the event is
        // signalled, then break the loop.
        // Ensure the messages are dispatched.
        AfxGetApp()->PumpMessage();
        if( !Lock.HasMessageInQueue() )
        {
            AfxMessageBox(_T("Wait complete"));
            break;
        }
    }
}  

WrapUp

I have prepared a demo application which simulates the error cases. This will help in quickly understanding the intended usage. MultiObjectLock supports up to MAXIMUM_WAIT_OBJECTS. You have to use the technique of creating multiple threads and wait on thread handles if you want to support more than that. I hope these classes are useful and are more aligned with the object oriented way of dealing with thread synchronization. I am eagerly waiting to see how it is being used. Thank you for the patient reading.

History

  • 10th February, 2013 - Released as a set of classes
  • 18th March, 2013 - Released as a DLL and made some minor fixes. The demo application uses this library.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
India India
A senior engineer who likes reading and coding Smile | :) . I can be reached at sudheeshps@gmail.com

Comments and Discussions

 
QuestionWait function results Pin
geoyar11-Mar-13 8:44
professionalgeoyar11-Mar-13 8:44 
AnswerRe: Wait function results Pin
Sudheesh.P.S12-Mar-13 5:12
Sudheesh.P.S12-Mar-13 5:12 
GeneralRe: Wait function results Pin
geoyar12-Mar-13 7:32
professionalgeoyar12-Mar-13 7:32 
QuestionI have some doubt. Pin
yafan10-Mar-13 6:17
yafan10-Mar-13 6:17 
AnswerRe: I have some doubt. Pin
Sudheesh.P.S11-Mar-13 5:30
Sudheesh.P.S11-Mar-13 5:30 
GeneralRe: I have some doubt. Pin
yafan11-Mar-13 8:51
yafan11-Mar-13 8:51 
Hi Sudhesh, thanks for your replies. I can sort of agree with you on 1 and 2 since they are subjective. However, I still have some doubt with your response to point 3.

The point I am making is can be phrased with the question: "Are C++ exceptions (i.e. throw/catch) thread safe".

In other words: if I have a thread that throws an exception because it is using your lock mechanism, and that sometime during the processing of that throw, another thread that is also trying to perform a lock to gain access to the same resource, again using your lock mechanism, and that thread also throws an exception, what happens to the throw from the first thread? There could now be 2 throws in flight. Which one is caught?

Are you absolutely sure that there are no issues with exceptions and thread safety? You seem to be saying that there isn't because there is no difference between your exception based framework and using return codes. This maybe true, but I'm questioning that assertion.

I'm just not sure that a user mode thread can prevent preemption even when it is processing a throw/catch. I couldn't find anything definitive on the subject, so I am not entirely sure.

From Googeling around, I did see that an uncaught exception can crash not only the thread but the entire process.

Also, I'm not sure this link is relevant to the question, but it does seem to imply that if you want catch an exception from multiple threads, from a primary thread, you need to transport the exception using the mechanism explained in this link:

http://msdn.microsoft.com/en-us/library/dd293602.aspx[^]


What do you think?

-y
GeneralRe: I have some doubt. Pin
Sudheesh.P.S12-Mar-13 4:58
Sudheesh.P.S12-Mar-13 4:58 
QuestionPerformance Considerations Pin
Jason Curl5-Mar-13 2:33
professionalJason Curl5-Mar-13 2:33 
AnswerRe: Performance Considerations Pin
Sudheesh.P.S5-Mar-13 5:19
Sudheesh.P.S5-Mar-13 5:19 
GeneralRe: Performance Considerations Pin
Jason Curl5-Mar-13 6:07
professionalJason Curl5-Mar-13 6:07 
GeneralRe: Performance Considerations Pin
Sudheesh.P.S6-Mar-13 4:03
Sudheesh.P.S6-Mar-13 4:03 
QuestionOpening sentence - I'd question the wisdom of using exceptions to replace return codes. Pin
Mike Diack5-Mar-13 1:27
Mike Diack5-Mar-13 1:27 
AnswerRe: Opening sentence - I'd question the wisdom of using exceptions to replace return codes. Pin
Sudheesh.P.S5-Mar-13 5:05
Sudheesh.P.S5-Mar-13 5:05 
GeneralInteresting approach Pin
H.Brydon9-Feb-13 15:57
professionalH.Brydon9-Feb-13 15:57 
GeneralRe: Interesting approach Pin
Sudheesh.P.S11-Feb-13 3:02
Sudheesh.P.S11-Feb-13 3:02 

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.