Threading and Multitasking
Win16
Win16 was a non-preemptive operating
system. This means that while several processes could be running at the
same time, if one process were to enter an infinite loop it could also lock
up all the others. Win16 would simulate multitasking in the way it dealt
with the message loop.
In a typical Win16 program, it's message loop would look something like
the following:
while( GetMessage( &Msg, 0, 0, NULL ) ) { TranslateMessage( &Msg ); DispatchMessage( &Msg ); }
Most of the program's idle time was spent here
in this loop. The 'trick' that Win16 performed was that when a program called
GetMessage, the GetMessage routine would actually permit another process
to execute a single loop of it's message loop. That other process would
call GetMessage, and it would happen again and again. This should be looked
at as a 'coordinated' effort between all processes.
As long as programs spent the majority of their time in the message loop,
each could be assured a reasonable amount of processor time. A problem however
would arise, when an application would perform a length process, and not
use it's message loop for some time. Under these circumstances, since GetMessage
was not being called, all the other processes would not have any processor
time.
Message Pump
In Win16, to overcome this problem,
something called a 'message pump' was implemented during the lengthy process.
For example, image the following code:
for( int I=0; I < 100000; I++ ) { DoSomethingElse(); }
Since there are no GetMessage routines, other processes are held up, and
even the process executing the loop would fail to process it's messages,
like WM_PAINT. The solution was to introduce a message pump. The message
pump is basically another message loop, executing during the lengthy process.
Instead of using GetMessage(), which waits for a message, PeekMessage was
used (which doesn't wait for a message before returning). The new function
would like:
MSG Msg; for( int I=0; I < 100000; I++ ) { while( PeekMessage( &Msg, 0, 0, NULL, PM_REMOVE ) ) { TranslateMessage( &Msg ); DispatchMessage( &Msg ); } DoSomethingElse(); }
Now, messages were pumped and all the processes could get processing time.
The process performing the loop could also process it's own messages, like
WM_PAINT, or the user clicking a CANCEL button.
Win32
With Win32, true multi-tasking was implemented
between processes. The message loop is still present, but it is no longer
responsible for permitting other processes to have a 'time slice' of the
CPU to perform their tasks. Now, the Windows kernel takes care of dividing
up the CPU time.
Since there is still only one message queue, there is still a side effect
to not calling the message loop in a win32 during a lengthy process. The
messages like WM_PAINT or CANCEL for that process will not be handled. The
message pump, implemented just like in Win16, is still the method to work
around this.
Threads
Win32 also introduced the concept
of a thread. A thread is not a completely separate process, but a
separate thread of execution. A separate process will start a new program
running, starting at it's main or WinMain function. A thread however, will
start a thread of execution on a single function. The SDK provides a function
called _beginthread, and MFC provides a function called AfxBeginThread.
For both _beginthread and AfxBeginThread, you pass a pointer to a function.
When the function returns, it continues processing as normal, but the function's
address you provided will also start executing simultaneously. You now have
2 threads of execution. When the 'threaded' function returns, the thread
terminates.
Timers versus Threads
Most times programmers want to take
advantage of the latest technology, simply because it's new. Threads are
no different. Threads can be difficult to manage and coordinate. A common
way to simulate multi-processing without threads is with the timer.
If you have a process that needs to be executed in a loop, you can take
the body of the loop and execute it once in response to the WM_TIMER event.
For example, consider:
for( int I=0; I < 500; I++ ) DrawFrame( I );
Imagine the code above draws a series of frames, for an animation sequence. Also, note how there's no message pump, so any WM_PAINT messages will not get processed. Now, convert the above loop into timer messages:
static int FrameNum; switch( Message ) { case WM_TIMER: DrawFrame( FrameNum++ ); if( FrameNum==500 ) { KillTimer( TimerID ); FrameNum=0; } break; }
The speed of the animation can be controlled by setting the periodic delay
for the timer, or terminated by killing the timer. Also, since this doesn't
interfere with the message loop, no message pump is needed.
Starting a Thread
As mentioned earlier, a new thread
is started by passing a function name to one of several thread-starting
functions. The new thread will start executing that function, and when that
function terminates, so will the thread. The following SDK functions start
a thread:
unsigned long _beginthread( void( __cdecl *start_address )(
void * ), unsigned stack_size, void *arglist );
Begins a thread, with default security. Start_address is a pointer
to the function to start the thread on, and arglist will become the
parameter to that function. stack_size indicates the stack size to
be provided to the new thread.
Returns the thread ID, or 0 upon failure.
unsigned long _beginthreadex( void *security, unsigned stack_size,
unsigned ( __stdcall *start_address )( void * ), void *arglist,
unsigned initflag, unsigned *thrdaddr );
Like _beginthread, except that security permits you to specify WinNT
security values, and can start a thread in a suspended state.
Returns the thread ID, or -1 upon failure.
HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter,
DWORD dwCreationFlags, LPDWORD lpThreadId );
Creates a thread with various attributes and security settings. If your
building an application that uses the threaded, statically-linked, C runtime
library (LIBCMT.LIB), you must not use this function.
Returns handle to a thread, or 0 upon failure.
The MFC library includes a function called AfxBeginThread, described later.
Example:
void SortArray( void* Array ) { SortStrings( (char**) Array ); Done++; } char *Array1[]={ "Bill", "Betty", John", "Franke", "Abe", NULL }; char *Array2[]={ "One", "Two", Three", "Four", "Five", NULL }; char *Array3[]={ "Smith", "Jones", Wilson", "Lee", "Kim", NULL }; int Done; SortEm() { Done=0; _beginthread( SortArray, Array1 ); _beginthread( SortArray, Array2 ); _beginthread( SortArray, Array3 ); while( Done != 3 ) // Wait for threads to terminate ; }
Note: In the above example, the three arrays will be sorted simultaneously,
but each sort will take roughly 3 times longer. In other words, there is
only so much raw CPU power available, and a thread divides that power.
Stopping a thread
When the function that was started
with the thread terminate normally, the thread will also terminate. Whatever
value the function returned (or was specified in an AfxEndThread function
call) will become the exit code of the thread.
The only way to terminate a thread is to do so from the thread itself. The
only way for a parent thread to terminate a child thread, is to communicate
it's request to the child thread, like through a global variable of some
sort.
To retrieve the exit code for a thread, you can use the GetExitCodeThread
function, from the parent thread.
Synchronizing threads
When two threads want to access
a single resource, such as a global variable, they must coordinate their
efforts. For example, if thread 1 where to attempt to get a buffer's string
length while thread two were putting a new string into the buffer, the results
can be unpredictable.
The API offers several methods and over 20 functions to implement this synchronization.
We'll look at just the 'critical section' technique.
VOID InitializeCriticalSection( LPCRITICAL_SECTION lpCriticalSection);
VOID EnterCriticalSection( LPCRITICAL_SECTION lpCriticalSection );
BOOL TryEnterCriticalSection( LPCRITICAL_SECTION lpCriticalSection
);
VOID LeaveCriticalSection( LPCRITICAL_SECTION lpCriticalSection );
VOID DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection );
In all cases, lpCriticalSection is the pointer to a variable declared
with the CRITICAL_SECTION modifier. Before anything else, the InitializeCriticalSection
function is called to mark the variable as a critical section.
Next, either EnterCriticalSection or TryEnterCriticalSection is called.
If these functions are succesful, the calling thread 'owns' the critical
section. If another thread attempts to call EnterCriticalSection it will
be suspended. If another thread trys to call TryEnterCriticalSection, it
will return, but with an error code. When the thread succesfully acquired
the critical section is done, it calls LeaveCriticalSection, and the next
caller to EnterCriticalSection or TryEnterCriticalSection will aquire that
critical section.
When all of the processing is done, the DeleteCriticalSection is called
to release the variable as a critical section.
A typical critical section variable in an object might look like (this is
termed a thread-safe class):
class String{ String() { InitializeCriticalSection( &Lock ); } ~String( {DeleteCriticalSection( &Lock); } int GetLength() { int Ret; EnterCriticalSection(&Lock); Ret=strlen(Str); LeaveCriticalSection(&Lock); } void Set( char *Src) { EnterCriticalSection(&Lock); Ret=strlen(Str); strcpy( Str, Src ); LeaveCriticalSection(&Lock); } private: CRITICAL_SECTION Lock; char Str[128]; }
In addition to the above 'locked critical section', the following other
basic approaches are used:
· Event objects, where threads wait for a specific event, usually
from another thread.
· Mutex objects, where threads wait for exclusive use of the object.
· Timer objects, where a thread can pause a certain period of time.
MFC and Thread Synchonization
MFC provides wrapper classes for
the SDK syncronization methods. The synchronization classes are:
CSyncObject, CSemaphore, CMutex, CCriticalSection, and Cevent
The synchronization access objects, which lock, unlock, or determine if
an item is locked, are:
CMultiLock and CSingleLock
Using the above class example in MFC, the code would now look like:
class String{ String() { InitializeCriticalSection( &Lock ); } ~String( {DeleteCriticalSection( &Lock); } int GetLength() { int Ret; CCSingleLock MyLock( &Critical ); MyLock.Lock(1); Ret=strlen(Str); } void Set( char *Src) { CCSingleLock MyLock( &Critical ); MyLock.Lock(1); Ret=strlen(Str); strcpy( Str, Src ); } private: CCriticalSection Critical; char Str[128]; }
Worker threads vs. User-Interface Threads
MFC makes a distinction between
user-interface threads and worker threads. A user-interface thread has a
user interface that can respond to user-events (it looks like a normal window)
and messages, while a worker thread does not. The 'Sort' demo above would
be an example of a worker thread, and also be the most common type of thread.
MFC provides two versions of the AfxBeginThread, using standard C++ overloading.
One is used to create a worker thread, and the other to create a user-interface
thread. The Sort examlple above describes a worker thread, and is very similar
to using AfcBeginThread to create one.
The version of AfxBeginThread that creates a user-interface thread looks
like the following:
CWinThread* AfxBeginThread( CRuntimeClass* pThreadClass,
int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize =
0, DWORD dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL );
In order to create a user-interface thread, you must perform the following
basic steps:
1. Create a new class, derived from CWinThread
2. Implement and use the DECLARE_DYNCREATE and IMPLEMENT_DYNCREATE macros
in the new class.
3. Over ride the InitInstance in the new class.
4. Call AfxBeginThread with the RUNTIME_CLASS of your new class.
The InitInstance of your new class is where the thread stays executing.
Once that function returns, the user interface thread will terminate.
Compiler Settings for Multi-Threading
In your Project Settings, in the
C++ tab, under the Code Generation category, Make sure that Multithreaded
is selected in the 'Use runtime library' value.
Variables and Threads
Since a thread is not a separate
process, a child thread has access to the same variables and their values
as it's parent and sibling threads do.
Knowing when to use threads
As mentioned earlier, threads can often
be replaced with a timer, or simply aren't needed at all. A function that
needs to sort three separate files will find that sort each file in order
will probably take less time than sorting each one in a thread, because
of the thread management overhead.
One possible use for threads, is quick user feedback. For example, as a
thread in the background sorts an array, the user might like to see the
items already sorted appear in a list, even though the sort isn't completed
yet.
One of the best examples of multi-threaded programming is web servers, like
FTP. When an FTP server program is running, it's main execution thread waits
for a socket (client( connection. When it gets a client connection, it starts
a separate thread to serve that connection, then returns right back to wait
for the next connection.