31. How do I get started working with threads?
Threads are really not that difficult, and worker threads
in particular are quite easy (worker threads don't have a user interface, they just do
work in the background). MFC isn't really threadsafe, and rather than spend time
remembering what I can and can't do with MFC and threads, I just avoid the use of MFC
within threads: my apps tend to have an MFC core with pure C/SDK worker threads.
Essentially a worker thread consists of a function, which is specified when the thread is
created (using CreateThread for an SDK program, or AfxBeginThread for an MFC program).
When the function exits, the thread is done. CreateThread looks complex, but most of its
parameters can be left at the defaults.
The only mildly tricky part is ensuring that the threads get killed off if your app is
closed down: this usually requires that your main thread set an "event" (see
CreateEvent/SetEvent) which the worker(s) can check - if it/they see the event set they
drop out of their processing loop, exit and therefore cease.
Of course if you want multiple threads accessing a single resource (such as a data
structure), you'll need to read up on synchronisation using Critical Section objects.
Actually getting to the shared object is trivial, as threads share the same address space.
Think of it like this : processes are the unit of addressability ; threads are the unit of
executability.
Check out these fragments of code, which are from a
multithreaded application I wrote recently. The application creates a closure event thus :
m_hCloseEvent = CreateEvent (NULL, TRUE,
FALSE, "AG61_BN9_RMM_CLOSE");
and this event is used when closing down the threads, as
we'll see below.
This is the part that starts the threads :
CWinThread * pNewThread;
...This code then appears inside a loop
which sets up the threads...
m_ateThreadTable [m_iThreadCount].iPortNum = iComNum;
m_ateThreadTable [m_iThreadCount].iThreadState = 0;
m_ateThreadTable [m_iThreadCount].hStopEvent = m_hCloseEvent;
m_ateThreadTable [m_iThreadCount].hCreatorWindow = GetSafeHwnd();
m_ateThreadTable [m_iThreadCount].uFlagBits = 0;
pNewThread = AfxBeginThread (WorkerRunFunction,
(LPVOID)&(m_ateThreadTable [m_iThreadCount]));
// If the new thread started ok, save it's pointer in our
// table, and clear its auto-delete flag so we can shut it
// down manually. MFC defaults to auto-deleting the thread
// object as soon as its run function exits, which can be
// problematic for the kind of controlled shutdown I want.
if (pNewThread)
{
m_ateThreadTable [m_iThreadCount].pThread = pNewThread;
pNewThread->m_bAutoDelete = FALSE;
}
So you can see that the code is setting up a table of
structures with data about each thread, then calling AfxBeginThread to create the thread,
passing it the address of the structure which contains information it will need
internally, such as the handle of the application closure event. The thread has no problem
accessing this structure, because all threads share the same address space. The
applications main thread then saves a pointer to the worker thread, which it can use when
shutting down to ensure that all threads close when instructed using the closure event :
if any threads fails to shutdown before some timeout, the app can call TerminateThread to
forcibly kill it.
The worker thread run function looks like this (note the
thread-local variables, which contain data specific to this thread):
static UINT __declspec( thread ) mg_uState ;
static int __declspec( thread ) mg_uFlags;
// ------------------------------------------------------------------------
UINT WorkerRunFunction (LPVOID pParam)
{
HANDLE hComPort = 0;
HANDLE aWaitHandles [2];
OVERLAPPED ovlRead = {0};
// Convert the generic void parameter pointer into a pointer
// to the actual data type passed.
TThreadEntry * pData = (TThreadEntry*)
pParam;
... Parameter validation code omitted...
// Create the event used for overlapped
i/o. This is a comms thread.
ovlRead.hEvent = CreateEvent (NULL, TRUE, FALSE, NULL);
// Our thread consists of a loop, waiting for either a character
// on the serial port or closure of the app. Note that the closure
// event is first, since WaitForMultipleObjects gives "precedence"
// starting from the zeroth end of its handle array.
aWaitHandles [0] = pData->hStopEvent;
aWaitHandles [1] = ovlRead.hEvent;
... Comms code (port setup) omitted...
while (!bStopped)
{
....< could additionally check the closure event here
>...
if (ReadFile (...)
{
// received data, store it and look at
the buffer.
}
else
{
dwErr = GetLastError();
switch (dwErr)
{
case ERROR_IO_PENDING :
// Wait for either data to arrive, or this
// thread to be terminated.
dwErr = WaitForMultipleObjects (2,
aWaitHandles,
FALSE,
INFINITE);
switch (dwErr)
{
case WAIT_OBJECT_0: // CLOSE event
bStopped = TRUE;
break;
case WAIT_OBJECT_0+1: // READ event
GetOverlappedResult (hComPort,
&ovlRead,
&dwBytesRead,
FALSE);
ResetEvent (ovlRead.hEvent);
... < do some processing here >...
Get the picture ? Our thread basically consists of a
loop, which sleeps until either a character is received or the app closure event gets set
by the main thread. So how does the main thread organise closure of its kiddies ? Like
this :
void CMyMainThreadDlg::DestroyThreads ()
{
HANDLE ahTempHandles [MAX_PORTS];
int iHandles;
// Now we have to wait _synchronously_ for all the threads to
// run to completion. Make up an array of thread handles for
// WaitForMultipleObjects to wait on.
iHandles = 0;
memset (ahTempHandles, 0, sizeof(ahTempHandles));
for (int i=0;
i < MAX_PORTS;
i++)
{
if (m_ateThreadTable [i].pThread)
{
ahTempHandles [iHandles]=
(m_ateThreadTable [i].pThread)-> m_hThread;
iHandles++;
}
}
if (iHandles > 0)
{
// There are threads to shut down - set the closure event.
SetEvent (m_hCloseEvent);
switch (WaitForMultipleObjects (iHandles,
ahTempHandles,
TRUE,
5000))
{
case WAIT_FAILED:
wsprintf
(m_szDebug, "WaitForMultipleObjects failed, GLE=%u",
GetLastError()) ;
DebugMessage (m_szDebug);
break;
case WAIT_OBJECT_0:
DebugMessage ("All COM threads closed OK");
break;
case WAIT_ABANDONED:
DebugMessage ("Wait abandoned");
break;
case WAIT_TIMEOUT:
DebugMessage ("Timed out waiting for COM threads");
HandleLaggards ();
break;
default:
break;
}
if (ResetEvent (m_hCloseEvent))
DebugMessage ("Reset close event for
next use");
else
{
wsprintf (m_szDebug, "attempt to
reset close event failed, GLE=%u",
GetLastError()) ;
DebugMessage (m_szDebug);
}
}
else
{
DebugMessage ("No active threads, exiting");
}
}
Additional Info :
The best source for information on the basics of threading is Jeffrey Richters book
"Advanced Windows". If you have the MSDN, you should also read the article
"Multithreading Dos and Don'ts".