Introduction
Multithreading and the necessary synchronization can sometimes
be quite challenging. Windows 2000 brings some new concepts into
play which doesnt make it really much easier to cope with this topic.
Processes, threads, fibers, jobs and whatelse is available to "help"
writing applications that do many things at the same time.
Most less experienced developers get lost while trying to figure
out what to use and when. After finally having figured out
how to multithread, the joy of debugging a multithreaded application
starts. Things start to get really funny when you finally have a
multithreaded solution up and running and it deadlocks on a multiprocessor
machine. Never assume that your multithreaded application behaves
correctly until it runs sucessfully on a multiprocessor machine.
There are many factors that can cause your concept
to fail: compiler optimizations due to missing or incomplete
variable declarations (e.g. missing volatile
), deadlocks
because of unexpected execution behavior, simple logic errors and many others.
Job-based Multithreading
In many cases a solution that creates one thread
for each job could just blow the system and achive the
opposite effect: a slowdown due to too many threads running.
Imagine a class to enumerate and process files and directories. A simple
implementation could just launch one thread for each directory
found to speed up processing.
The idea is good, but can you imagine what will happen
if your customer has limited throughput on disk I/O but his
disc-controller is quite good in caching? The result will be an
explosion of threads because directory and file enumeration
is fast because of the disc cache, but processing of the files contents
is slow as a result of the limited disc I/O.
A better solution in most cases is a limited set of threads
(known as a thread pool). A queue of jobs feeds the threads with work.
While Windows 2000 and Windows XP support
thread pooling and
jobs, NT4 and Win9x do not. Which makes it just a bit more difficult
to write cross-Windows applications.
The CJobManager
class brings the advantages of a
thread pool and jobs down to the C++ level and is fully cross-windows
(although I actually did'nt test it under Win9x).
CJobManager
provides a very flexible interface
to be of use in most applications that "require" multithreading
while still supporting GUI processing. In one of the comming articles
I will present a more sophisticated approach with guarded execution
and secure multithreaded object orientation.
But for now, lets stick with the simple
implementation of CJobManager
.
The Class
CJobManager
defaults to be used as a
singleton, thus has no public constructor/destructor.
Your application gets a CJobManager
object by
calling the static memberfunction
CJobManager::GetJobManager(BOOL bNewManager, int nThreads)
.
Anyway, by specifying TRUE
as the first
argument, a new instance of a CJobManager
will be allocated
and initialized. Be sure to save the returned pointer, as the function
does not track created instances.
The most important function is
CJobManager::AddJob(LPTHREADEDJOB pJob)
.
The argument pJob
points to a simple structure which carries
all information needed to process a job. This structure is the point
where you can extend the implementation of a job to accomodate your needs.
typedef struct tagThreadedJob
{
LPTHREAD_START_ROUTINE pFunction;
PJOBMANAGER_FEEDBACK pfnFeedback;
LPARAM lParam1;
LPARAM lParam2;
LPARAM lParam3;
LPARAM lParam4;
LPARAM lParam5;
} THREADEDJOB, * LPTHREADEDJOB;
Note: You can not remove a single job once it is queued.
The class implementation defaults to be
non-terminating to its threads. This means any
instance of CJobManager
will wait for completion
of the running jobs without interrupting the threads. You can
however, specify that you definitly want the threads to be
terminated when the application needs to, by calling SetTerminate(TRUE)
.
You can at any time increase or decrease the number
of threads (default is eight) in a instance
of CJobManager
by calling SetThreads(nThreads)
.
When in non-terminating mode, the function will wait for
each thread to finish its job before removing it from the pool.
If your application wants to wait for completion of the jobs
it can call CJobManager::WaitCompletion(BOOL bCurrentJobsOnly)
.
WaitCompletion returns only after all jobs have been processed.
It does allow message processing by calling the
CJobManager::ProcessMessages()
function.
When your application needs to poll for
completion of the jobs it calls CJobManager::IsCompleted()
.
Together with CJobManager::Wait(DWORD dwMillisec, BOOL bGUIprocessing)
you can construct wait loops to do some processing while
waiting for completion of the jobs.
while(!CJobManager::GetJobManager()->IsCompleted())
{
CJobManager::GetJobManager()->Wait(250, TRUE);
}
The CJobManager
class also supports
a feedback function that can be called from within a jobs
processing function. The current job provides this function
through Feedback(LPVOID pFeedback)
. This function will
return TRUE
when no feedback function is provided, else
it calls the function and returns the return value. The intention of
this feedback function is on one hand, to allow for real feedback on
actual processed data which can be provided to the feedback function
with the LPVOID
argument, and on the other hand to allow ending the
current job by returning FALSE
.
Call CJobManager::SetFeedback(PJOBMANAGER_FEEDBACK pfn)
to set a feedback function.
The Sample
The included sample uses a modified
CFileInfo class
from Antonio Tejada Lacaci to enumerate all files from a
choosen drive. The sample starts processing at the root of
the drive and adds a job for every found directory. These jobs
are processed by the default eight threads. The processing can be
interrupted at any time with the "Stop" button. Every 250 milliseconds
the dialog updates the count of found files.
Note: the sample leaks memory when you close the dialog while
its still running because some objects are not freed when
terminating the CJobManager
.
Compatibility
Tested under Windows XP, Windows 2000 and NT4SP6 on single
and dual processor machines. UNICODE compatible
but not included in this sample. VC6.
Update
May 14, 2002 - bug in WaitCompletion()
fixed.