<

Simple Multithreading in Visual C++

Published on
55,425 Points
43,925 Views
5 Endorsements
Last Modified:
Awarded
DanRollins
Here's a summary of how to use multithreading, with short, easy-to-follow example code in C++.  

Before you embark on this journey, I suggest that you read Multithreading -- When and Why because odds are, you really don't need to create additional threads in your program.  You just think you do.  Assuming that you still want to take the plunge, know this:  I'm going to assume that you have isolated a specific task for a worker thread that has no direct interaction with the User Interface -- as advised in that article.  That lets me cut out nearly all of the muddled discussion you will read in MSDN and elsewhere.
Example program runningYour Thread-Using Object
The first thing you might think of doing is to try to derive from CWinThread.  Forget it.  That does nothing for you.  We won't be using the message loop or any of its other whiz-bang features and in my experience handling EE threading questions, deriving from it will just add to your confusion.  

Instead, think of what the thread will do... and create an object that represents that action.  For instance, if it is a tool to connect to a web server and read a web page, then call it CPageReader.  If it polls for activity from an external device, then call it CDeviceMonitor.  The fact that it does its task by using a worker thread is (or at least should be considered to be) incidental to the functionality.

A Simple Example
Let's start with the simple basics -- no C++ object at all, just a naked thread.  This sequence starts a thread that simply updates a counter 20 times per second.

//---- First example: Using global vars and fn
int gnCurValue;
int gnMaxValue;
BOOL gfStopNow;

UINT MyThreadProc( LPVOID pParam )
{
    while ( !gfStopNow && (gnCurValue < gnMaxValue) ) {
        gnCurValue++;
        Sleep( 50 ); // would do some work here
    }
    return(1);
}
void CThreadTesterDlg::OnBnClickedStart()
{
    gnCurValue= 0;
    gnMaxValue= 5000;
    gfStopNow= 0;
    m_ctlStatus.SetWindowText("Starting...");
    SetTimer( 1234, 333, 0 ); // 3 times per second

    AfxBeginThread( MyThreadProc, 0 ); // <<== START THE THREAD
}

void CThreadTesterDlg::OnBnClickedStop()
{
    gfStopNow= TRUE;
    KillTimer( 1234 );
    m_ctlStatus.SetWindowText( "STOPPED" );
}

void CThreadTesterDlg::OnTimer(UINT_PTR nIDEvent)
{
    CString sStatusMsg;
    sStatusMsg.Format("Running: %d", gnCurValue );
    m_ctlStatus.SetWindowText( sStatusMsg );

    CDialog::OnTimer(nIDEvent);
}

Open in new window

We used AfxBeginThread on line 22 to spin off the secondary worker thread.  We could just as easily have used the CreateThread API function.  The former ends up at the latter (by way of the _beginthread C-Runtime function).

When AfxBeginThread is called, the code in MyThreadProc (starting at line 6) begins executing (actually, it starts at the next available time-slice, but the effect is about the same in this case).  As with most ThreadProc functions, MyThreadProc is set up with a loop to get the work done.  For demonstration purposes, it simply checks for completion of its task (counter reaches a maximum, or programmatic abort request).  When that function returns, the thread stops executing.

Also for demonstration purposes, the "outside world" communicates with the ThreadProc via global variables.  The ThreadProc updates the status counter, and the U/I thread (the CThreadTesterDlg object) can display that counter.  And to kill the thread, the controlling program needs only set the gfStopNow variable to TRUE to make it exit from the loop and return.

Here's an important point:  You might consider having the ThreadProc display the status counter itself.  That might work in this case, but that would be going in the wrong direction.  The ThreadProc should be a worker thread and not interact with the U/I; in this case, that means it should not update the screen.  Instead, I've set up a Window Timer that monitors the gnCurValue variable three times per second and sends text to the screen.  It could just as easily update a Progress Control or add lines to an Edit box, or whatever.

I can't emphasize this enough.  You might think you'll be clever and use PostMessage in your thread to control the U/I.  But then one day, your U/I gets sluggish (for any number of reasons) and you end up with 10,000 items clogging up the message queue, blocking normal user interaction and making your program appear to crash.  Make your worker threads U/I-less to avoid whole categories of problems.

Basically, that's all there is too it:
Write your ThreadProc with the main logic in a loop to do its work.
Provide a means to tell it to exit that loop and therefore end the thread, and...
Do all of your screen updates in the main U/I thread.

A More Sophisticated Example
Here's a more realistic example... at least it's one that might benefit by using multithreading.  The next example collects a list of all files in a directory and all of its subdirectories.  Such a sequence, starting from the root, is likely to take more than a few seconds on most computers these days.  If this was a requirement of your program, you might want to get it started as soon as possible, but not wait until the entire list was finished before moving on with other U/I tasks (incidently, you may have noticed that the Windows Explorer uses this defer-and-update technique when displaying its directory tree).

This time, I'm going to use a C++ class object that encapsulates the task and its data.
class CFileCollector
{
public:
    CFileCollector::CFileCollector() {
        m_fAbortNow= m_nCntFiles= m_nCntDirs= 0;
    };

    CStringArray m_asAllNames;
    BOOL         m_fDone;
    int          m_nCntFiles;
    int          m_nCntDirs;
    CString      m_sStartDir;
    CString      m_sStatusText;
    CWinThread*  m_pcThread;

    void Start( LPCSTR szStartDir ) {
        m_fAbortNow= FALSE;
        m_nCntFiles= m_nCntDirs= 0;
        m_sStatusText= "Starting...";
        m_sStartDir= szStartDir;
        m_pcThread= 
        AfxBeginThread( FileCollectorThreadProc, this );
    }
    void StopNow(void) {
        m_fAbortNow= TRUE;
    }
private:
    BOOL m_fAbortNow;
    static UINT FileCollectorThreadProc( LPVOID pParam ) {
        CFileCollector* pThis= (CFileCollector*)pParam;
        UINT nRet= pThis->DoThreadProc();     // get out of 'static mode'
        return( nRet );
    }
    UINT DoThreadProc() {
        m_fDone= FALSE;
        AllFilesInDirAndSubdirs( m_sStartDir ); // curse and recurse!
        m_sStatusText.Format("DONE: Found %d Files in %d Folders", m_nCntFiles, m_nCntDirs );
        if ( m_fAbortNow ) m_sStatusText= "Halted by User";
        m_fDone= TRUE;
        return( 0 );
    }

    void AllFilesInDirAndSubdirs( LPCSTR szDir ) {
        CFileFind cFF;
        CString sWildcard= CString(szDir)+ "\\*.*";
        BOOL fFound= cFF.FindFile( sWildcard );
        while( fFound && !m_fAbortNow ) {  // check for abort request
            fFound= cFF.FindNextFile();
            if ( cFF.IsDots() ) { // ignore . and ..
                continue;
            }
            if ( cFF.IsDirectory() ) {  // if directory...
                m_nCntDirs++;
                AllFilesInDirAndSubdirs( cFF.GetFilePath() ); // recurse!
                continue;
            }
            // else, regular file
            m_asAllNames.Add( cFF.GetFilePath() ); // add one filename to list
            m_nCntFiles++;
            m_sStatusText.Format("Found Folders: %6d Files: %9d", m_nCntDirs, m_nCntFiles );
        }
        cFF.Close();
    }
};

Open in new window

I used an important "trick" in that example, and it's worth knowing.  The ThreadProc must be a regular  _cdecl function, meaning it can be a global function (not within an object) or an object member function that is declared as static.  In lines 29 to 33, I create a static function that I can use as a "gateway" into the object -- a technique that is useful in many situations.  See  How to provide a CALLBACK Function Into a C++ Class Object for a detailed description of the technique.

The AllFilesInDirAndSubdirs function (lines 43 to 63) is your standard garden-variety recursive function for traversing a directory tree.  The one special (at least relevant to this discussion) part is that this function is, in effect, the inner loop of the ThreadProc.  Thus, I must put my m_fAbortNow check here (line 47).

As before, the ThreadProc does nothing that is U/I-related.  It makes available, in its containing CFileCollector object, the current counter of files and directories, and a ready-to-display status text variable.  And as before, I'm using a Window Timer in the main window thread to do the actual display work.

Here's an example of using the object:
#define IDT_Status 1234

void CThreadTesterDlg::OnBnClickedStartthread()
{
    GetDlgItem(IDPB_StartThread)->EnableWindow( FALSE);
    GetDlgItem(IDPB_StopThread)->EnableWindow( TRUE );
    SetTimer( IDT_Status, 100, 0 ); // 10 times per second

    m_cFileCollector.Start( "C:" ); // <<<=== GO!!!
}

void CThreadTesterDlg::OnBnClickedStopthread()
{
    m_cFileCollector.StopNow();  // <<<===STOP!!!

    GetDlgItem(IDPB_StopThread)->EnableWindow( FALSE );
    GetDlgItem(IDPB_StartThread)->EnableWindow( TRUE );
}
//-------------------------------- screen updates occur here
void CThreadTesterDlg::OnTimer(UINT_PTR nIDEvent)
{
    m_ctlStatus.SetWindowText( m_cFileCollector.m_sStatusText );
    if ( m_cFileCollector.m_fDone ) {
        KillTimer( IDT_Status );
        GetDlgItem(IDPB_StopThread)->EnableWindow( FALSE );
        GetDlgItem(IDPB_StartThread)->EnableWindow( TRUE );
    }
    CDialog::OnTimer(nIDEvent);
}

Open in new window

Notes:
In a real-live program, you might want to begin displaying the collected filenames or, perhaps adding them to a ListView or something like that.  The point of spinning the task off to a thread would be that you could, for instance, display the first part of the list before the entire list was generated.
If you run the program a couple of times, you'll see something that may seem strange.  The first time you run it, it may take a looooong time to complete, several minutes even.  Then if you run it again, it finishes in just a few seconds.  It appears that the system caches the file-and-directory information for you.  You'll find that after doing a full disk scan like this, interaction with the Windows Explorer is faster, too.
Killing a Recalcitrant Thread
The correct way to terminate a thread is to cause it to exit from its ThreadProc.  In a perfect world, a check for a flag like m_fAbortNow in the inner loop would always work.  However... it's not entirely uncommon for a worker thread to get stuck for a long time in a system call (e.g., waiting for a response to an Internet request) or to have blocked itself as it waits for something else to happen (as when using WaitForSingleObject).

So, if you find yourself in a situation where you must stop the thread, for instance when you must restart after a critical error, and it fails to stop normally, you may need to resort to a forced termination.

In our CFileCollector object, we saved a CWinThread pointer that will provide the thread's handle.  We can use that for two purposes.  First, we can find out if the thread has exited cleanly.  And if not (and after giving it time to do so), then we can forcibly terminate it:
m_cFileCollector.StopNow(); // try to stop normally
Sleep(1000);

HANDLE hThread=m_cFileCollector.m_pcThread->m_hThread;
DWORD nExitCode;
BOOL fRet= ::GetExitCodeThread( hThread, &nExitCode );

if ( fRet && nExitCode==STILL_ACTIVE ) { // did not die!
    TerminateThread( hThread, -1 ); // <<== Kill it (but not cleanly)
}

Open in new window

The problem with this code (and it can be quite a significant problem) is that it does not let the thread clean up after itself.  It never falls off the end of its ThreadProc (or other, deeply-nested procedure frames), so object destructors do not get called.

In the CFileCollector example, the stack frame could be quite deep because of the recursion in the AllFilesInDirAndSubdirs function.  If the program was 10 levels deep then there would be 10 copies of the CFileFind object that would not get a chance to close or destruct cleanly.  If this forced thread termination was taking place just before program exit, then that's no big deal.  But if you were to blindly use TerminateThread as a "normal" way to shut down the thread, you would have yourself a nasty resource leak.

In a future article, I'll cover some of the more advanced topics of thread usage... critical sections to prevent task-switches at the wrong time, using synchronization objects to avoid race conditions, and so forth.

References:

Multithreading -- Why and When

How to provide a CALLBACK Function Into a C++ Class Object

AfxBeginThread  (MFC function)
http://msdn.microsoft.com/en-us/library/s3w9x78e(VS.80).aspx

_beginthread  C-Runtime function
http://msdn.microsoft.com/en-us/library/kdzttdcb(VS.71).aspx

CreateThread function
http://msdn.microsoft.com/en-us/library/ms682453(VS.85).aspx
ThreadProc Callback Function
http://msdn.microsoft.com/en-us/library/ms686736(VS.85).aspx

TerminateThread function
http://msdn.microsoft.com/en-us/library/ms686717(VS.85).aspx

GetExitCodeThread function
http://msdn.microsoft.com/en-us/library/ms683190(VS.85).aspx

=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
If you liked this article and want to see more from this author,  please click the Yes button near the:
      Was this article helpful?
label that is just below and to the right of this text.   Thanks!
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
5
Comment
Author:DanRollins
[X]
Welcome to Experts Exchange

Add your voice to the tech community where 5M+ people just like you are talking about what matters.

  • Help others & share knowledge
  • Earn cash & points
  • Learn & ask questions
0 Comments

Featured Post

Creating Instructional Tutorials  

For Any Use & On Any Platform

Contextual Guidance at the moment of need helps your employees/users adopt software o& achieve even the most complex tasks instantly. Boost knowledge retention, software adoption & employee engagement with easy solution.

Join & Write a Comment

This is Part 3 in a 3-part series on Experts Exchange to discuss error handling in VBA code written for Excel. Part 1 of this series discussed basic error handling code using VBA. http://www.experts-exchange.com/videos/1478/Excel-Error-Handlin…
Exchange organizations may use the Journaling Agent of the Transport Service to archive messages going through Exchange. However, if the Transport Service is integrated with some email content management application (such as an antispam), the admini…

Keep in touch with Experts Exchange

Tech news and trends delivered to your inbox every month