Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / MFC

Guide to Choose and Implement a Worker Thread

4.64/5 (4 votes)
30 Jul 2020CPOL4 min read 8.3K  
A guide to choosing and implementing a worker thread in your application
This article intends to help programmers to choose and implement a worker thread in a Win32 / MFC application.

Introduction

First, there are several thread functions: _beginthread(), CreateThread, AfxBeginThread. What should I choose? To decide, we have to enumerate them.

Using the Code

  • _beginthread - Besides the sister _beginthreadex function, is part of C run - time library
    In this link, you can find a small example of how to use them in a real situation.
  • CreateThread - This is a Win32 API function.

A simple sample is listed below:

C++
class CSample
{
public:
  CSample() { m_hThread = 0; }
  ~CSample() { CloseHandle(m_hThread); }
  bool Create()
  {
    m_hThread = CreateThread(0, 0, ThreadFunction, this, 0, 0);
    if(! m_hThread)
    {
      return false;        // Could not create thread
    }
    return true;
  }
            
private:
  HANDLE m_hThread;

private:
  static DWORD WINAPI ThreadFunction(LPVOID pvParam);
};

and thread function:

C++
DWORD CSample::ThreadFunction(LPVOID pvParam)
{
    // do some useful things
}

Using:

C++
CSample sample;
if (! Create())
    AfxMessageBox(_T("Could not create thread !"));

If your project is a MFC project, you better use AfxBeginThread function which is part of MFC.

Before you setup a thread method, you will have to take care of two things:

  1. a way to control the thread method from outside of your child thread
  2. a way to know the main thread what is the state of your child thread

And you need a callback function or a static method in order to run it inside of your thread. Depending on what you need to do inside your thread, the implementation could be little different. Suppose you have to do a long time looping code, let's say you need to download something from the internet. This could take time. Here is the code for this kind of implementation:

C++
// CMyDialog header
#define WMU_NOTIFYTHREAD                        (WM_APP + 12)    // notification message 
                                                                 // from thread
#define THREADUPDATESTARTED                     1
#define THREADUPDATESTOPPED                     2
#define THREADUPDATEINFO                        3

class CMyDialog : public CDialog
{
    .....

protected:
    static UINT WorkerThread(LPVOID lpParam);        // this is the static method used 
                                                     // inside of child thread

protected:
    BOOL m_bThreadActive;                // the variable is used to know 
                                         // the state of the child thread
    volatile BOOL m_bRunThread;          // the variable is used to control 
                                         // the child thread from outside
    CWinThread* m_pWinThread;            // CWinThread object used with AfxBeginThread

    .....

        // Generated message map functions
protected:
    //{{AFX_MSG(CMyDialog)
    afx_msg void OnDestroy();
    afx_msg LRESULT OnNotifyThread(WPARAM wParam, LPARAM lParam);
    //}}AFX_MSG
};

The implementation looks like this:

C++
// CMyDialog implementation

BEGIN_MESSAGE_MAP(CMyDialog, CDialog)
    //{{AFX_MSG_MAP(CMyDialog)
    ON_WM_DESTROY()
    //}}AFX_MSG_MAP
    ON_MESSAGE(WMU_NOTIFYTHREADUPDATE, &CMyDialog::OnNotifyThread)
END_MESSAGE_MAP()

CMyDialog::CMyDialog()
    :m_pWinThread(NULL)        // initialize all variables
    , m_bRunThread(FALSE)
    , m_bThreadActive(FALSE)
{
    //
}

CMyDialog::StartThread()
{
    // create thread as suspended
    m_pWinThread = AfxBeginThread(&CMyDialog::WorkerThread, (LPVOID)this, 
                   THREAD_PRIORITY_BELOW_NORMAL, CREATE_SUSPENDED, 0, NULL);
    // setup additional information
    m_pWinThread->m_bAutoDelete = TRUE;      // this will tell the thread 
                                             // to auto clean at thread exit
    m_bRunThread = TRUE;
    m_pWinThread->ResumeThread();            // start the thread
}

CMyDialog::StopThread()
{
    m_bRunThread = FALSE;                    // setup this flag as FALSE will stop the 
                                             // looping inside of thread and so the thread
}

and as WorkerThread method, you can have a kind of:

C++
UINT CMyDialog::WorkerThread(LPVOID lpParam)
{
    CMyDialog* pDlg = (CMyDialog*)lpParam;
    if (NULL == pDlg || NULL == pDlg->GetSafeHwnd())
        return 1;    // fail thread

    ::PostMessage(pDlg->GetSafeHwnd(), 
          WMU_NOTIFYTHREAD, THREADUPDATESTARTED, 0); // notify main thread that child thread
                                                     // is started

    BOOL bJobDone = FALSE;
    while (pDlg->m_bRunThread && ! bJobDone)         // Run loop inside thread
    {
        // do actual work
        // if the job has been done, then set up bJobDone as TRUE
    }

    ::PostMessage(pDlg->GetSafeHwnd(), 
         WMU_NOTIFYTHREAD, THREADUPDATESTOPPED, 0);  // notify main thread that child thread 
                                                     // is stopped

    return 0;
}

Also, we have to receive the notification from child thread:

C++
LRESULT CMyDialog::OnNotifyThread(WPARAM wParam, LPARAM lParam)
{
    switch (wParam)
    {
    case THREADUPDATESTARTED:
        m_bThreadActive = TRUE;
        break;
    case THREADUPDATESTOPPED:
        m_bRunThread = FALSE;
        m_bThreadActive = FALSE;
        break;
    }

    return 1;
}

You also have to take care not to close your app(dialog) until you are sure that child thread is stopped:

C++
void CMyDialog::OnDestroy()
{
    CDialog::OnDestroy();

    // TODO: Add your message handler code here

    if (NULL != m_pWinThread)        // if thread is still active
    {
        m_bRunThread = FALSE;        // close the looping inside thread
        switch (WaitForSingleObject(m_pWinThread->m_hThread, 3000)) // wait for thread 
                                                                    // to close for 3 seconds
        {
        case WAIT_OBJECT_0:
            TRACE(_T("The worker thread just finished\n"));
            break;
        case WAIT_TIMEOUT:
            TRACE(_T("Cannot stop gracefully the worker thread\n"));
            break;
        }
    }
}

There is another situation inside thread method: you can have there a waitable function. Here is a sample:

C++
UINT AFX_CDECL CMyDialog::WorkerThread(LPVOID lpParam)
{
    CMyDialog* pDlg = (CMyDialog*)lpParam;
    if (NULL == pDlg || NULL == pDlg->GetSafeHwnd())
        return 1;    // fail thread

    ::PostMessage(pDlg->GetSafeHwnd(), WMU_NOTIFYTHREAD, THREADUPDATESTARTED, 0); // notify 
                                                    // main thread that child thread is started

    CArray<HANDLE, HANDLE&> arrHandle;
    arrHandle.Add(pDlg->m_EventChange.m_hObject);
    arrHandle.Add(pDlg->m_EventStop.m_hObject);

    BOOL bContinue = TRUE;
    while (bContinue)
    {
        const DWORD dwResult = WaitForMultipleObjects(arrHandle.GetSize(), 
                               arrHandle.GetData(), FALSE, INFINITE);
        switch (dwResult)
        {
        case WAIT_OBJECT_0:            // arrHandle elem 0 (pDlg->m_EventChange.m_hObject)
            // do something useful here
            break;
        case WAIT_OBJECT_0 + 1:        // arrHandle elem 1 (pDlg->m_EventStop.m_hObject)
            bContinue = FALSE;
            break;
        }
    }

    ::PostMessage(pDlg->GetSafeHwnd(), 
           WMU_NOTIFYTHREAD, THREADUPDATESTOPPED, 0); // notify main thread that 
                                                      // child thread is stopped

    return 0;
}

As you see the code, some changes must be made in this case. First, the child thread is stopped by a CEvent, and is controlled by a CEvent. In this way, we get rid of m_bRunThread variable. Instead, we have to add other two, CEvent type:

C++
// CMyDialog header
class CMyDialog : public CDialog
{
    ....
    ....

protected:
//  volatile BOOL m_bRunThread;            // no need it anymore
    CEvent m_EventStop;
    CEvent m_EventChange;
    CWinThread* m_pWinThread;
    BOOL m_bThreadActive;                  // the variable is used to know the 
                                           // state of the child thread

    ....
}

In such a way, stopping thread code is modifying as follows:

C++
void CMyDialog::StopThread()
{
    if (NULL != m_pWinThread)              // if thread is still active
    {
        m_EventStop.SetEvent();
        switch (WaitForSingleObject(&m_EventStop, 3000))        // wait for thread 
                                                                // to close for 3 seconds
        {
        case WAIT_OBJECT_0:
            TRACE(_T("The worker thread just finished\n"));
            break;
        case WAIT_TIMEOUT:
            TRACE(_T("Cannot stop gracefully the worker thread\n"));
            break;
        }
    }
}

To run the job inside of your child thread will be done by:

C++
void CMyDialog::RunThreadJob()
{
    m_EventChange.SetEvent();                // this will start the inside thread job
}

The starting thread method will be slightly different:

C++
CMyDialog::StartThread()
{
    m_EventStop.ResetEvent();
    m_EventChange.ResetEvent();
    // create thread as suspended
    m_pWinThread = AfxBeginThread(&CMyDialog::WorkerThread, 
                   (LPVOID)this, THREAD_PRIORITY_BELOW_NORMAL, CREATE_SUSPENDED, 0, NULL);
    // setup additional information
    m_pWinThread->m_bAutoDelete = TRUE;      // this will tell the thread 
                                             // to auto clean at thread exit
    m_pWinThread->ResumeThread();            // start the thread
}

Even so, we still have to wait for the thread to be stopped at our app(dialog) exit:

C++
void CMyDialog::OnDestroy()
{
    CDialog::OnDestroy();

    // TODO: Add your message handler code here

    StopThread();
}

What if we need multiple threads ? We still have to control the threads and notify the main dialog regarding the state of every thread. To handle all these, we have to modify the header accordingly:

C++
// CMyDialog header

CMyDialog::CMyDialog()
{
    ....

private:
//    BOOL m_bThreadActive;                               // in this case, we don't need 
                                                          // this anymore, we know if there 
                                                          // is any thread active by 
                                                          // checking m_arrThreads.GetSize()
    volatile BOOL m_bRunThread;                           // the variable is used to control
                                                          // the child thread from outside
    CTypedPtrArray<CObArray, CWinThread*> m_arrThreads;   // array of CWinThread objects
    ....
}

The static working thread method could look like this:

C++
UINT CMyDialog::WorkerThread(LPVOID lpParam)
{
    CMyDialog* pDlg = (CMyDialog*)lpParam;
    if (NULL == pDlg || NULL == pDlg->GetSafeHwnd())
        return 1;    // fail thread

    while (pDlg->m_bRunThread)          // Run loop inside thread
    {
        ::PostMessage(pDlg->GetSafeHwnd(), WMU_NOTIFYTHREAD, THREADUPDATEINFO, 
                     (LPARAM)::GetCurrentThreadId()));
        // do hard work job
    }

    return 0;
}

We receive in the main thread the child thread notifications:

C++
LRESULT CMyDialog::OnNotifyThread(WPARAM wParam, LPARAM lParam)
{
    switch (wParam)
    {
    case THREADUPDATEINFO:
        TRACE("Get message from thread with id: %d\n", lParam);
        break;
    }

    return 1;
}

The starting thread method should look like this:

C++
void CMyDialog::StartThread()
{
    CWinThread* pThread = AfxBeginThread(&CMyDialog::WorkerThread, (LPVOID)this, 
                          THREAD_PRIORITY_BELOW_NORMAL, 0, 0, NULL);
    m_arrThreads.Add(pThread);    // add new created thread to m_arrThreads array
}

Every time we call this method, we create a new thread, and when you want to stop all threads can do:

C++
void CMyDialog::StopThreads()
{
    const int nSize = m_arrThreads.GetSize();
    if (nSize > 0)
    {
        m_bRunThread = FALSE; // stop looping inside of thread method 
        HANDLE* pHandles = new HANDLE[nSize];
        for (int nIndex = 0; nIndex < nSize; ++nIndex)
            pHandles[nIndex] = m_arrThreads[nIndex]->m_hThread;
        ::WaitForMultipleObjects(nSize, pHandles, TRUE, INFINITE);

        m_arrThreads.RemoveAll();
        delete[] pHandles;
    }
}

When you process and access data from more than main thread is involved in the synchronization. In simple words, the syncronization means that you must take care to access a resource only once at a time.
If the shared resource is hosted in the main thread, then you can do syncronization from one or from multiple threads by simply using PostMessage from your child thread(s), which I did in every exposed example. The syncronization is done by using message table. If the shared resource must be accessed by concurrency between threads, you should use Mutex or Critical Section Objects.

Fortunately, this is not such a common situation. A good practice is to use mutex or critical sections only if you really need it. And InterlockExchange as well. However, if the situation is requesting, you can synchronize m_bRunThread with InterlockExchange just like this: change m_bRunThread variable as static:

C++
// CMyDialog header

class CMyDialog : public CDialog
{

......

static BOOL m_bRunThread;

}

and:

C++
// CMyDialog implementation

BOOL CMyDialog::m_bRunThread = FALSE;

and every time when you need to change its value, you write:

C++
::InterlockedExchange((LONG)&m_bRunThread, 0);

// or 

::InterlockedExchange((LONG)&m_bRunThread, 1);

Until now, I exemplified just simple thread routines. If you need to do complicated things inside of your thread method, then the code will be longer and this must signal that you have a design issue. In this case, you will have to derive your own CThread class where you can handle whatever you need. In this CThread derived class, you can implement other features for your thread (pause thread, or other sophisticated syncronization methods).
You can find here (on www.codeproject.com) a lot of CThread derived classes for this purpose. This approach is not the goal of this article.

Points of Interest

I hope this article will be useful for those who want to choose and implement a thread for the first time.

History

  • 30th July, 2020 - First release

License

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