VC++
MFC Thread Tutorial: _beginthreadex,
WaitForSingleObject, pausing, resuming, and stopping threads
|
|
Arjay
Download
Example Source Code
Abstract
Frequently in the Visual
C++ forum and multithreaded forum,
questions are raised on how to start
and stop threads and
how to manipulate MFC
UI elements from within a worker thread. This article
will take the reader through the steps of building a
simple MFC dialog
application called StartStop. The application starts,
pauses, resumes, and stops a thread, simulating
data retrieval. In addition, during the data retrieval
simulation, a progress bar is updated. Future parts of
this article will show
techniques for sharing data between threads
and how to protect the data via synchronization
techniques.
Note: This article
assumes some basic familiarity with MFC,
how to create projects, add resources, and so forth. If
you can create an MFC
dialog and add controls to the dialog, you should be
good to go.
Creating the Basic
Project
Open up Visual
Studio .Net 2003 and create an MFC
dialog project called StartStop.
Because it's a good idea
to keep the UI code separate from the threading code, you
are going to put the threading code inside a ProgressMgr
class, but first you are going to create the basic UI
'shell' code that creates the basic button handlers and UI
variables that allow you to enable/disable the buttons,
change the button text, and manipulate the progress
control. You know, the UI stuff.
Create the UI
The UI for the
application will be a simple dialog consisting of three
buttons and a progress control. To create the UI, open the
resource editor and click on the IDD_STARTSTOP_DIALOG.
Next, delete the OK button and add two buttons with the ID
of the first button ID_STARTPAUSERESUME and the second one
as ID_STOP. Also, add a progress control. You should end
up with something like Figure 1.
Figure 1
The plan will be to add
some button and progress control variables, button message
handlers, and some user-defined messages to let the UI to
know when to update the progress control and when the
thread has finished. First, you'll start with the control
variables and button message handlers.
Add the UI control
variables
Next, you'll add the
control variables to allow you to enable/disable the
buttons. Although many programmers use the Win32
GetDlgItem() API to connect the controls, it's preferred
to leverage MFC's DDX mechanism and let MFC handle the
plumbing of the controls. Once you've tried this method,
you may never go back to GetDlgItem().
Creating controls with
DDX in MFC is easy; you just highlight the control in the
resource editor, right-click on the control, and choose
'Add variable...'. Here's how to do it for the Start
button.
- Open the dialog in the
resource editor.
- Right-click on the
Start button and choose 'Add variable...'.
- The 'Add Member
Variable Wizard bo?= StartStop' dialog will
appear (see Figure 2).
- Under 'Variable
name:', ENTER m_btnStartPauseResume.
- Press Finish.
Figure 2
Repeat for the Stop
button and Progress control
Do the same for the stop
button and progress control, entering m_btnStop and
m_ctrlProgress for the variable names.
Add the Start
and Stop button message
handlers
After creating the
control variables, in the dialog editor just double-click
on the Start, Stop,
and Cancel buttons to create the message handlers. You
should end up with empty handlers as follows:
void CStartStopDlg::OnBnClickedStartPauseResume()
{
}
void CStartStopDlg::OnBnClickedStop()
{
}
void CStartStopDlg::OnBnClickedCancel()
{
OnCancel();
}
Toggling the Start/Pause
button state
You want the start
button to also function as a pause
and resume button. To
do this, you need to keep track of what 'mode' the button
is in. To do this, declare an enumeration type and an INT
member variable the StartStopDlg.h file. Don't forget to
initialize this variable in the constructor to FALSE. You
should end up with the following:
protected:
enum ThreadStatus { TS_STOPPED, TS_START, TS_PAUSE, TS_RESUME };
INT m_ThreadState;
CStartStopDlg::CStartStopDlg(CWnd* pParent )
: CDialog(CStartStopDlg::IDD, pParent)
, m_ThreadState( TS_STOPPED )
{
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}
Next, add a method to
toggle the button display text and toggle state.
INT CStartStopDlg::ToggleSPRState( )
{
if( TS_RESUME == m_ThreadState )
{
m_ThreadState = TS_START;
}
m_ThreadState++;
CString sButtonText = _T("");
switch( m_ThreadState )
{
case TS_START:
sButtonText = _T("Pause");
break;
case TS_PAUSE:
sButtonText = _T("Resume");
break;
case TS_RESUME:
sButtonText = _T("Pause");
break;
default:
ASSERT( 0 );
}
m_btnStartPause.SetWindowText( sButtonText );
return m_ThreadState;
}
And a method to reset the
StartPause button state and button text.
void CStartStopDlg::ResetSPRState( )
{
m_ThreadState = TS_STOPPED;
m_btnStartPause.SetWindowText( _T("Start") );
m_ctrlProgress.SetStep( 0 );
}
Stub Out the Start/Pause
and Stop Button Handlers
You want the UI to
properly disable and enable the buttons, plus you want to
hook up the ToggleStartPauseButton method you just created
to the Start button handler. Change the button handlers to
the following:
void CStartStopDlg::OnBnClickedStartPauseResume()
{
m_btnStartPause.EnableWindow( FALSE );
m_btnStop.EnableWindow( FALSE );
switch( ToggleSPRState( ) )
{
case TS_START:
Sleep( 5000 );
break;
case TS_PAUSE:
Sleep( 5000 );
break;
case TS_RESUME:
Sleep( 5000 );
break;
default
ASSERT( 0 );
}
m_btnStartPause.EnableWindow( TRUE );
m_btnStop.EnableWindow( TRUE );
}
void CStartStopDlg::OnBnClickedStop()
{
m_btnStop.EnableWindow( FALSE );
m_btnStartPause.EnableWindow( FALSE );
Sleep( 1000 );
m_btnStop.EnableWindow( TRUE );
m_btnStartPause.EnableWindow( TRUE );
}
void CStartStopDlg::OnBnClickedCancel()
{
OnCancel();
}
Tip: You may wonder
why the buttons have control variables that allow you to
enable and disable the buttons inside the button
handlers. It is true that you are going to only going to
start, pause, and stop a thread and generally these
operations are very quick, especially in your simple
application. However, in a production application thread
start up or shut down may be significant, so you want to
disable the button (albeit temporarily) to let the user
know that something is happening and also prevent users
from 'double-clicking' on the button.
Test the Dialog
Test the dialog to make
sure the start button toggles to pause properly and both
start and stop buttons disable and enable properly.
Note: I took this
screen shot after clicking the start button.
Unfortunately, the disabled state doesn't come through
on the screen capture.
Figure 3
Tip: It's a good
idea to test the UI thread controlling functionality
separated from the threading code. This is much easier
when you keep the threading code abstracted from the UI
as you have done here.
Finishing Up the UI
Before you move on to
implement the threading code, there are a couple of
remaining UI tucancode.net that you need to complete.
Initialize the progress
control
For the progress control
to work, you need to initialize it first. To do this, y ou
simply call the CProgressDlg.SetRange32() method. You'll
do this at the end of the OnInitDialog( ) method.
BOOL CStartStopDlg::OnInitDialog()
{
CDialog::OnInitDialog();
SetIcon(m_hIcon, TRUE);
SetIcon(m_hIcon, FALSE);
m_ctrlProgress.SetRange32( 0, 1000 );
m_ctrlProgress.SetPos( 0 );
return TRUE;
}
Defining the user
defined messages
MFC
UI controls are not threadsafe; that means you shouldn't
manipulate the control from within the worker thread. For
example, you aren't allowed to call m_ctrlProgress.StepIt()
from inside the worker thread. Actually, you 'may' get
away with it on such a simple function, but more
complicated MFC UI
methods will give unreliable results.
Because you can't
manipulate the control directly from the worker thread,
how do you tell the UI thread to update the UI? Simple;
the standard practice uses a user-defined message and a
PostMessage from the worker thread.
Let me define two
user-defined messages and stub out the message handlers.
- Open up the stdafx.h
file and add:
#define WM_USER_INC_PROGRESS WM_USER + 1
#define WM_USER_THREAD_COMPLETED WM_USER + 2
Next
- Define the message
handler prototypes. In CStartStopDlg.h, add:
afx_msg LRESULT OnIncProgress( WPARAM, LPARAM );
afx_msg LRESULT OnThreadCompleted( WPARAM, LPARAM );
Important: Pay
attention to the user-defined message signature
because it has changed since VC6. In VC6, it used to
be defined as afx_msg void OnMessageHandler( );.
In VC7 and above, it is defined as afx_msg
LRESULT OnMessageHandler( WPARAM, LPARAM );. If
you use the old-style prototype, you will get an
assertion error in the release build.
- Add the message map
entries to the message map:
BEGIN_MESSAGE_MAP(CSimpleThreadDlg, CDialog)
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_BN_CLICKED(ID_STARTSTOP, OnBnClickedStartstop)
ON_BN_CLICKED(IDCANCEL, OnBnClickedCancel)
ON_MESSAGE( WM_USER_INC_PROGRESS, OnIncProgress )
ON_MESSAGE( WM_USER_THREAD_COMPLETED,
OnThreadCompleted )
END_MESSAGE_MAP()
- Create the message
handlers:
LRESULT CStartStopDlg::OnIncProgress( WPARAM, LPARAM )
{
m_ctrlProgress.StepIt( );
return 1;
}
LRESULT CStartStopDlg::OnThreadCompleted( WPARAM, LPARAM )
{
ResetStartPauseButton( );
return 1;
}
Creating the Thread Code
Now that the UI code has
been stubbed out nicely (and tested), you can move on to
the threading code. Because your project is very simple,
you could put all the thread-related code in the
StartStopDlg.cpp file. But instead of doing this, keep the
threading code decoupled from the UI as much as possible.
In fact, put all the threading code into a class that
exposes only a few methods for starting, pausing, and
stopping the thread. Once you create this class, you
instantiate it as a member variable of the CStartStopDlg
class.
Stubbing Out the
CProgressMgr Class
Use the Add class wizard
to create a new class. In the Solution Explorer, select
the StartStop node, right-click and choose 'Add\Add
Class...' The 'Add Class' dialog will appear. Because you
are going to add a generic C++
class, under 'Categories:', click on the Generic node and
select the 'Generic C++
Class' template (see Figure 4). Press Open.
Figure 4
Next, the 'Generic C++
Class Wizard appears. Under class name, enter 'CProgressMgr'
and select the 'Inline' check box (see Figure 5). The
inline option only creates a .h file without a .cpp file.
Press the 'Finish' button. Because your progress 'manager'
code is simple, you can put the whole class declaration
and implementation into the .h file—the 'Inline'
checkbox option does this for you.
Figure 5
The wizard creates a
single ProgressMgr.h file containing:
#pragma once
class CProgressMgr
{
public:
CProgressMgr(void)
{
}
~CProgressMgr(void)
{
}
};
Add the Start, Pause,
and Stop method stubs
The UI is going to
interact with this class to Start, Pause, and Stop the
thread, so it's time to stub out these methods. Make the
following changes to the CProgressMgr class.
#pragma once
class CProgressMgr
{
public:
CProgressMgr( )
{
}
~CProgressMgr( )
{
}
public:
HRESULT Pause( ) { Sleep( 1000 ); return S_OK; }
HRESULT Resume( ) { Sleep( 1000 ); return S_OK; }
HRESULT Start( HWND hWnd ) { Sleep( 1000 ); return S_OK; }
HRESULT Stop( ) { Sleep( 1000 ); return S_OK; }
private:
HWND m_hWnd;
};
The Start method takes a
hWnd param. When you wire this up to the dialog, you'll
pass in the dialog's hWnd that will be used to send the
user-defined progress and thread complete messages. Notice
that I've added Sleep( 1000 ) statements in the method
stubs. This is so you can test the UI after connecting up
the class methods. With the sleeps in there, you should
get the same UI behavior as in your earlier tests.
Wiring Up the
CProgressMgr Code to the UI
Now that the progress
manager code has been stubbed out, you can connect it to
the UI.
Add the #include
ProgressMgr.h declaration
Modify the include
section of theStartStopDlg.cpp and StartStop.cpp files. to
add the #include ProgressMgr.h. You should end up with the
following for StartStopDlg.cpp and StartStop.cpp files:
#include "stdafx.h"
#include "StartStop.h"
#include "ProgressMgr.h"
#include "StartStopDlg.h"
#include ".\startstopdlg.h"
Declare the CProgressMgr
instance
Add the CProgressMgr
m_ProgressMgr variable to the CStartStopDlg class.
protected:
HICON m_hIcon;
BOOL m_bToggleStartPause;
CProgressMgr m_ProgressMgr;
Wire up the message
handlers
Change the Start
and Stop button
handlers to connect with the progress manager.
void CStartStopDlg::OnBnClickedStartPauseResume()
{
m_btnStartPause.EnableWindow( FALSE );
m_btnStop.EnableWindow( FALSE );
switch( ToggleSPRState( ) )
{
case TS_START:
m_ProgressMgr.Start( GetSafeHwnd( ) );
break;
case TS_PAUSE:
m_ProgressMgr.Pause( );
break;
case TS_RESUME:
m_ProgressMgr.Resume( );
break;
default:
ASSERT( 0 );
}
m_btnStartPause.EnableWindow( TRUE );
m_btnStop.EnableWindow( TRUE );
}
void CStartStopDlg::OnBnClickedStop()
{
m_btnStartPause.EnableWindow( FALSE );
m_btnStop.EnableWindow( FALSE );
m_ProgressMgr.Stop( );
ResetStartPauseButton( );
m_btnStartPause.EnableWindow( TRUE );
m_btnStop.EnableWindow( TRUE );
}
At this point, the UI
code is completed. No more UI changes should be necessary.
Threading Preliminaries
One of the goals of the article
is to allow you to start,
pause, resume, and stop
a thread. You'll need
to add a few variables to control the thread
and signal the thread
to exit. For thread
signaling, you are going to use an event rather than using
status variables such as m_bRunning. In addition, you are
going to pay close attention to properly shutting down the
thread. Once you signal the thread
to exit, you need to ensure that it has exited properly.
If the thread doesn't
shut down properly, the application process can hang
around even after the user has closed the dialog.
Defining the
thread-related variables
Define a couple of
handles for the thread and the shutdown event. Modify the
CProgressMgr class and add the following:
HANDLE m_hThread;
HANDLE m_hShutdownEvent;
Initialize the member
variables in the constructor
Although it's always good
practice to initialize variables, with thread related code
it's especially important.
CProgressMgr( )
: m_hThread( NULL )
, m_hShutdownEvent( ::CreateEvent( NULL, TRUE, FALSE, NULL ) )
{
}
Take care of thread
cleanup
Modify the destructor to
add the cleanup code. You'll define the ShutdownThread
method next.
~CProgressMgr( )
{
ShutdownThread( );
::CloseHandle( m_hShutdownEvent );
}
Shutdown thread
implementation
The shutdown code simply
checks whether the thread
handle is null. If it isn't, it means the thread
is running and signals the thread to exit by setting the
ShutdownEvent and then waiting for the thread
to exit. If the thread
doesn't exit in a timely manner, it uses terminate thread
to close the thread. In your sample code, you don't care
if the thread gets terminated, but in a real app, you may
want to check whether the thread was terminated. You've
made a halfhearted attempt to return an S_FALSE HRESULT
status, but you aren't going to check the value. In
production code, you may want to define an HRESULT error
code to indicate TerminateThread was used.
HRESULT ShutdownThread( )
{
HRESULT hr = S_OK;
if( NULL != m_hThread )
{
::SetEvent( m_hShutdownEvent );
::ResumeThread( m_hThread );
if ( WAIT_TIMEOUT == WaitForSingleObject( m_hThread, 1000 ) )
{
::TerminateThread( m_hThread, -1000 );
hr = S_FALSE;
}
::CloseHandle( m_hThread );
m_hThread = NULL;
}
::ResetEvent( m_hShutdownEvent );
return hr;
}
A few words about
TerminateThread: MSDN offers pretty strict warnings
against using TerminateThread of TerminateProcess APIs.
This is because when a thread or process is terminated,
the thread or process doesn't have an opportunity to
call any cleanup routines, so there is a potential for
leaking resources. See MSDN for more information.
In your use here, the
thread proc is going to be a static method of the
CProgressMgr class and your cleanup will occur even if
you have to terminate the thread because the
CProgressMgr destructor will get called when the dialog
gets destructed. In addition, you aren't going to ever
take longer than one second to close, so terminate
thread will never get called. You've coded it this way
to provide more of a production code example.
Create Thread
implementation
Along with the shutdown thread
method, create another helper method to create
the thread. The third
parameter is your ThreadProc. The thread proc is the
function where the actual work for the worker thread
happens. Notice that you pass the 'this' pointer during thread
creation. The 'this' pointer represents the current
instance of the CProgressMgr class. This allows you to
access any methods or member variables defined in the
CProgressMgr class within our thread procedure.
Note: To use _beginthreadex,
be sure to add #include <process.h> to the
stdafx.h file.
HRESULT CreateThread( )
{
if( NULL == (m_hThread = (HANDLE)_beginthreadex(
NULL,
0,
ThreadProc,
static_cast<LPVOID>( this ),
0,
NULL) ) )
{
return HRESULT_FROM_WIN32( ::GetLastError( ) );
}
return S_OK;
}
Thread Procedure
implementation
The thread procedure for
this example is simple. All you want to do is simulate
performing some work in the worker thread and updating the
progress in the UI (via the method NotifyUI that
internally uses PostMessage( ) to send post a
message to the UI). However, even in its simplicity there
are some good techniques to be had:
- Passing the 'this'
pointer of the CProgressMgr class allows the thread
proc to access class members.
- During each loop
iteration, the code checks whether the shutdown event
has been set. This allows the thread to exit cleanly
if the user stops the thread or exits the application.
static UINT WINAPI ThreadProc( LPVOID lpContext )
{
CProgressMgr* pProgressMgr
= reinterpret_cast< CProgressMgr* >( lpContext );
for( UINT uCount = 0; uCount < 100; uCount++ )
{
if( WAIT_OBJECT_0 ==
WaitForSingleObject( pProgressMgr->GetShutdownEvent( ), 0 ) )
{
return 1;
}
pProgressMgr->NotifyUI( NOTIFY_PROGRESS );
Sleep( 75 );
}
pProgressMgr->NotifyUI( NOTIFY_THREAD_COMPLETED );
return 0;
}
Go to page
Building out the Start,
Stop, Pause, and Resume methods
At this point, building
out the public interface methods becomes almost trivial.
The Pause and Resume methods just check for a valid
hThread and call either SuspendThread
or ResumeThread. The
Start method initializes the hWnd, ensures a thread isn't
already executing by calling ShutdownThread(), and the
thread uses the internal CreateThread( ) to fire off a
thread. Stop just calls your internal ShutdownThread
method.
void Pause( )
{
if( NULL != m_hThread )
{
::SuspendThread( m_hThread );
}
}
void Resume( )
{
if( NULL != m_hThread )
{
::ResumeThread( m_hThread );
}
}
HRESULT Start( HWND hWnd )
{
HRESULT hr = S_OK;
m_hWnd = hWnd;
if( SUCCEEDED( hr = ShutdownThread( ) ) )
{
hr = CreateThread( );
}
return hr;
}
HRESULT Stop( )
{
return ShutdownThread( );
}
Completing the Code
To complete the code, you
just need to create the GetShutdownEvent and NotifyUI
methods.
HANDLE& GetShutdownEvent( )
{
return m_hShutdownEvent;
}
void NotifyUI( UINT uNotificationType )
{
if( ::IsWindow( m_hWnd ) )
{
switch( uNotificationType )
{
case NOTIFY_INC_PROGRESS:
::PostMessage( m_hWnd, WM_USER_INC_PROGRESS, 0, 0 );
break;
case NOTIFY_THREAD_COMPLETED:
::PostMessage( m_hWnd, WM_USER_THREAD_COMPLETED, 0, 0 );
break;
default:
ASSERT( 0 );
}
}
}
Program Operation
Press F5 to run the
application in debug mode. Once the dialog opens, press
the 'Start' button. You'll notice the button changes to
'Pause' and the progress bar begins to increment. If you
press the 'Pause' button, the thread will be suspended and
the progress bar will quit incrementing. Pressing 'Resume'
will resume the secondary thread and the progress bar will
continue to increment. When the thread has completed its
simulation loop, it posts a thread
completed message to the UI; this causes the UI to reset
(in other words, the progress bar is cleared and the
button text is set to 'Start'). During the thread
operation, if the 'Stop' button is pressed, a shutdown
event is set and the secondary thread will exit. Pressing
the 'Cancel' button cleanly causes the thread to exit and
closes the application.
|