Multithreading is growing in importance in modern
programming for a variety of reasons, not the least of which being that
Windows supports multithreading. While C++ does not feature built-in
support for multithreading, it can be used to created multithreaded
programs, which is the subject of this article. It is taken from
chapter three of The Art of C++, written by Herbert Schildt (McGraw-Hill/Osborne, 2004; ISBN: 0072255129).
Multithreading
is becoming an increasingly important part of modern programming. One
reason for this is that multithreading enables a program to make the
best use of available CPU cycles, thus allowing very efficient programs
to be written. Another reason is that multithreading is a natural
choice for handling event-driven code, which is so common in today’s
highly distributed, networked, GUI-based environments. Of course, the
fact that the most widely used operating system, Windows, supports
multithreading is also a factor. Whatever the reasons, the increased
use of multithreading is changing the way that programmers think about
the fundamental architecture of a program. Although C++ does not
contain built-in support for multithreaded programs, it is right at
home in this arena.
Because of its growing importance, this chapter explores using C++
to create multithreaded programs. It does so by developing two
multithreaded applications. The first is a thread control panel, which
you can use to control the execution of threads within a program. This
is both an interesting demonstration of multithreading and a practical
tool that you can use when developing multithreaded applications. The
second example shows how to apply multithreading to a practical example
by creating a modified version of the garbage collector from Chapter 2
that runs in a background thread.
This chapter also serves another purpose: it shows how adept C++ is
at interfacing directly to the operating system. In some other
languages, such as Java, there is a layer of processing between your
program and the OS. This layer adds overhead that can be unacceptable
for some types of programs, such as those used in a real-time
environment. In sharp contrast, C++ has direct access to low-level
functionality provided by the operating system. This is one of the
reasons C++ can produce higher performance code.
What Is Multithreading?
Before beginning, it is necessary to define precisely what is meant by the term multithreading.
Multithreading is a specialized form of multitasking. In general, there
are two types of multitasking: process-based and thread-based. A process is, in essence, a program that is executing. Thus, process-based multitasking
is the feature that allows your computer to run two or more programs
concurrently. For example, it is process-based multitasking that allows
you to run a word processor at the same time you are using a
spreadsheet or browsing the Internet. In process-based multitasking, a
program is the smallest unit of code that can be dispatched by the
scheduler.
A thread is a dispatchable unit of executable code. The name comes from the concept of a “thread of execution.” In a thread-based
multitasking environment, all processes have at least one thread, but
they can have more. This means that a single program can perform two or
more tasks concurrently. For instance, a text editor can be formatting
text at the same time that it is printing, as long as these two actions
are being performed by two separate threads. The differences between
process-based and thread-based multitasking can be summarized like
this: Process-based multitasking handles the concurrent execution of
programs. Thread-based multitasking deals with the concurrent execution
of pieces of the same program.
In the preceding discussions, it is important to clarify that true
concurrent execution is possible only in a multiple-CPU system in which
each process or thread has unrestricted access to a CPU. For single CPU
systems, which constitute the vast majority of systems in use today,
only the appearance of simultaneous execution is achieved. In a single
CPU system, each process or thread receives a portion of the CPU’s
time, with the amount of time determined by several factors, including
the priority of the process or thread. Although truly concurrent
execution does not exist on most computers, when writing multithreaded
programs, you should assume that it does. This is because you can’t
know the precise order in which separate threads will be executed, or
if they will execute in the same sequence twice. Thus, its best to
program as if true concurrent execution is the case.
Multithreading Changes the Architecture of a Program
Multithreading changes the fundamental architecture of a program.
Unlike a single-threaded program that executes in a strictly linear
fashion, a multithreaded program executes portions of itself
concurrently. Thus, all multithreaded programs include an element of
parallelism. Consequently, a major issue in multithreaded programs is
managing the interaction of the threads.
As explained earlier, all processes have at least one thread of execution, which is called the main thread.
The main thread is created when your program begins. In a multithreaded
program, the main thread creates one or more child threads. Thus, each
multithreaded process starts with one thread of execution and then
creates one or more additional threads. In a properly designed program,
each thread represents a single logical unit of activity.
The principal advantage of multithreading is that it enables you to
write very efficient programs because it lets you utilize the idle time
that is present in most programs. Most I/O devices, whether they are
network ports, disk drives, or the keyboard, are much slower than the
CPU. Often, a program will spend a majority of its execution time
waiting to send or receive data. With the careful use of
multithreading, your program can execute another task during this idle
time. For example, while one part of your program is sending a file
over the Internet, another part can be reading keyboard input, and
still another can be buffering the next block of data to send.
Why Doesn’t C++ Contain Built-In Support for Multithreading?
C++ does not contain any built-in support for multithreaded
applications. Instead, it relies entirely upon the operating system to
provide this feature. Given that both Java and C# provide built-in
support for multithreading, it is natural to ask why this isn’t also
the case for C++. The answers are efficiency, control, and the range of
applications to which C++ is applied. Let’s examine each.
By not building in support for multithreading, C++ does not attempt
to define a “one size fits all” solution. Instead, C++ allows you to
directly utilize the multithreading features provided by the operating
system. This approach means that your programs can be multithreaded in
the most efficient means supported by the execution environment.
Because many multitasking environments offer rich support for
multithreading, being able to access that support is crucial to the
creation of high-performance, multithreaded programs.
Using operating system functions to support
multithreading gives you access to the full range of control offered by
the execution environment. Consider Windows. It defines a rich set of
thread-related functions that enable finely grained control over the
creation and management of a thread. For example, Windows has several
ways to control access to a shared resource, including semaphores,
mutexes, event objects, waitable timers, and critical sections. This
level of flexibility cannot be easily designed into a language because
the capabilities of operating systems differ. Thus, language-level
support for multithreading usually means offering only a “lowest common
denominator” of features. With C++, you gain access to all the features
that the operating system provides. This is a major advantage when
writing high-performance code.
C++ was designed for all types of programming, from
embedded systems in which there is no operating system in the execution
environment to highly distributed, GUI-based end-user applications and
everything in between. Therefore, C++ cannot place significant
constraints on its execution environment. Building in support for
multithreading would have inherently limited C++ to only those
environments that supported it and thus prevented C++ from being used
to create software for nonthreaded environments.
In the final analysis, not building in support of
multithreading is a major advantage for C++ because it enables programs
to be written in the most efficient way possible for the target
execution environment. Remember, C++ is all about power. In the case of
multithreading, it is definitely a situation in which “less is more.”
What Operating System and Compiler?
Because C++ relies on the operating system to provide
support for multithreaded programming, it is necessary to choose an
operating system as the target for the multithreaded applications in
this chapter. Because Windows is the most widely used operating system
in the world, it is the operating system used in this chapter. However,
much of the information can be generalized to any OS that supports
multithreading.
Because Visual C++ is arguably the most widely used
compiler for producing Windows programs, it is the compiler required by
the examples in this chapter. The importance of this is made apparent
in the following section. However, if you are using another compiler,
the code can be easily adapted to accommodate it.
NOTE
The examples in this chapter assume a basic, working knowledge of Windows programming.
Windows offers a wide array of Application Programming
Interface (API) functions that support multithreading. Many readers
will be at least somewhat familiar with the multithreading functions
offered by Windows, but for those who are not, an overview of those
used in this chapter is presented here. Keep in mind that Windows
provides many other multithreading-based functions that you might want
to explore on your own.
To use Windows’ multithreading functions, you must include <windows.h> in your program.
Creating and Terminating a Thread
To create a thread, the Windows API supplies the CreateThread( ) function. Its prototype is shown here:
HANDLE CreateThread(LPSECURITY_ATTRIBUTES secAttr,
SIZE_T stackSize,
LPTHREAD_START_ROUTINE threadFunc,
LPVOID param,
DWORD flags,
LPDWORD threadID);
Here, secAttr is a pointer to a set of security attributes pertaining to the thread. However, if secAttr is NULL, then the default security descriptor is used.
Each thread has its own stack. You can specify the size of the new thread’s stack in bytes using the stackSize parameter.
If this integer value is zero, then the thread will be given a stack
that is the same size as the creating thread. In this case, the stack
will be expanded, if necessary. (Specifying zero is the common approach
taken to thread stack size.)
Each thread of execution begins with a call to a function, called the thread function,
within the creating process. Execution of the thread continues until
the thread function returns. The address of this function (that is, the
entry point to the thread) is specified in threadFunc. All thread functions must have this prototype:
DWORD WINAPI threadfunc(LPVOID param);
Any argument that you need to pass to the new thread is specified in CreateThread( )’s param.
This 32-bit value is received by the thread function in its parameter.
This parameter may be used for any purpose. The function returns its
exit status.
The flags parameter determines the execution state of the
thread. If it is zero, the thread begins execution immediately. If it
is CREATE_SUSPEND, the thread is created in a suspended state, awaiting
execution. (It may be started using a call to ResumeThread( ),
discussed later.)
The identifier associated with a thread is returned in the long integer pointed to by threadID.
The function returns a handle to the thread if successful
or NULL if a failure occurs. The thread handle can be explicitly
destroyed by calling CloseHandle( ). Otherwise, it will be destroyed
automatically when the parent process ends.
As just explained, a thread of execution terminates when its entry
function returns. The process may also terminate the thread manually,
using either TerminateThread( ) or ExitThread( ), whose prototypes are
shown here:
BOOL TerminateThread(HANDLE thread, DWORD status);
VOID ExitThread(DWORD status);
For TerminateThread( ), thread is the handle
of the thread to be terminated. ExitThread( ) can only be used to
terminate the thread that calls ExitThread( ). For both functions, status is the termination status. TerminateThread( ) returns nonzero if successful and zero otherwise.
Calling ExitThread( ) is functionally equivalent to
allowing a thread function to return normally. This means that the
stack is properly reset. When a thread is terminated using
TerminateThread( ), it is stopped immediately and does not perform any
special cleanup activities. Also, TerminateThread( ) may stop a thread
during an important operation. For these reasons, it is usually best
(and easiest) to let a thread terminate normally when its entry
function returns.
The Visual C++ Alternatives to CreateThread( ) and ExitThread( )
Although CreateThread( ) and ExitThread( ) are the
Windows API functions used to create and terminate a thread, we won’t
be using them in this chapter! The reason is that when these functions
are used with Visual C++ (and possibly other Windows-compatible
compilers), they can result in memory leaks, the loss of a small amount
of memory. For Visual C++, if a multithreaded program utilizes C/C++
standard library functions and uses CreateThread( ) and ExitThread( ),
then small amounts of memory are lost. (If your program does not use
the C/C++ standard library, then no such losses will occur.) To
eliminate this problem, you must use functions defined by the C/C++
runtime library to start and stop threads rather than those specified
by the Win32 API. These functions parallel CreateThread( ) and
ExitThread( ), but do not generate a memory leak.
Note
If you are using a compiler other than Visual C++, check its documentation to determine if you need to bypass CreateThread( ) and ExitThread( ) and how to do so, if necessary.
The Visual C++ alternatives to CreateThread( ) and
ExitThread( ) are _beginthreadex( ) and _endthreadex( ). Both require
the header file <process.h>. Here is the prototype for
_beginthreadex( ):
uintptr_t _beginthreadex(void *secAttr, unsigned stackSize,
unsigned (__stdcall *threadFunc)(void *),
void *param, unsigned flags,
unsigned *threadID);
As you can see, the parameters to _beginthreadex( )
parallel those to CreateThread( ). Furthermore, they have the same
meaning as those specified by CreateThread( ). secAttr is a pointer to a set of security attributes pertaining to the thread. However, if secAttr is NULL, then the default security descriptor is used. The size of the new thread’s stack, in bytes, is passed in stackSize
parameter. If this value is zero, then the thread will be given a stack
that is the same size as the main thread of the process that creates it.
The address of the thread function (that is, the entry point to the thread) is specified in threadFunc. For _beginthreadex( ), a thread function must have this prototype:
unsigned __stdcall threadfunc(void * param);
This prototype is functionally equivalent to the one for
CreateThread( ), but it uses different type names. Any argument that
you need to pass to the new thread is specified in the param parameter.
The flags parameter determines the execution state of the
thread. If it is zero, the thread begins execution immediately. If it
is CREATE_SUSPEND, the thread is created in a suspended state, awaiting
execution. (It may be started using a call to ResumeThread( ).) The
identifier associated with a thread is returned in the double word
pointed to by threadID.
The function returns a handle to the thread if successful or zero if
a failure occurs. The type uintptr_t specifies a Visual C++ type
capable of holding a pointer or handle.
The prototype for _endthreadex( ) is shown here:
void _endthreadex(unsigned status);
It functions just like ExitThread( ) by stopping the thread and returning the exit code specified in status.
Because the most widely used compiler for Windows is Visual C++, the
examples in this chapter will use _beginthreadex( ) and _endthreadex( )
rather their equivalent API functions. If you are using a compiler
other than Visual C++, simply substitute CreateThread( ) and EndThread(
).
When using _beginthreadex( ) and _endthreadex( ), you must remember
to link in the multithreaded library. This will vary from compiler to
compiler. Here are some examples. When using the Visual C++
command-line compiler, include the –MT option. To use the multithreaded
library from the Visual C++ 6 IDE, first activate the Project |
Settings property sheet. Then, select the C/C++ tab. Next, select Code
Generation from the Category list box and then choose Multithreaded in
the Use Runtime Library list box. For Visual C++ 7 .NET IDE, select
Project | Properties. Next, select the C/C++ entry and highlight Code
Generation. Finally, choose Multi-threaded as the runtime library.
Suspending and Resuming a Thread
A thread of execution can be suspended by calling SuspendThread( ).
It can be resumed by calling ResumeThread( ). The prototypes for these
functions are shown here:
DWORD SuspendThread(HANDLE hThread);
DWORD ResumeThread(HANDLE hThread);
For both functions, the handle to the thread is passed in hThread.
Each thread of execution has associated with it a suspend count.
If this count is zero, then the thread is not suspended. If it is
nonzero, the thread is in a suspended state. Each call to
SuspendThread( ) increments the suspend count. Each call to
ResumeThread( ) decrements the suspend count. A suspended thread will
resume only when its suspend count has reached zero. Therefore, to
resume a suspended thread implies that there must be the same number of
calls to ResumeThread( ) as there have been calls to SuspendThread( ).
Both functions return the thread’s previous suspend count or –1 if an error occurs.
Changing the Priority of a Thread
In Windows, each thread has associated with it a priority setting. A
thread’s priority determines how much CPU time a thread receives. Low
priority threads receive little time. High priority threads receive a
lot. Of course, how much CPU time a thread receives has a profound
impact on its execution characteristics and its interaction with other
threads currently executing in the system.
In Windows, a thread’s priority setting is the combination of two
values: the overall priority class of the process and the priority
setting of the individual thread relative to that priority class. That
is, a thread’s actual priority is determined by combining the process’s
priority class with the thread’s individual priority level. Each is
examined next.
By default, a process is given a priority class of normal, and most
programs remain in the normal priority class throughout their execution
lifetime. Although neither of the examples in this chapter changes the
priority class, a brief overview of the thread priority classes is
given here in the interest of completeness.
Windows defines six priority classes, which correspond to the value shown here, in order of highest to lowest priority:
|
REALTIME_PRIORITY_CLASS |
|
HIGH_PRIORITY_CLASS |
|
ABOVE_NORMAL_PRIORITY_CLASS |
|
NORMAL_PRIORITY_CLASS |
|
BELOW_NORMAL_PRIORITY_CLASS |
|
IDLE_PRIORITY_CLASS |
Programs are given the NORMAL_PRIORITY_CLASS by default. Usually,
you won’t need to alter the priority class of your program. In fact,
changing a process’ priority class can have negative consequences on
the overall performance of the computer system. For example, if you
increase a program’s priority class to REALTIME_PRIORITY_CLASS, it will
dominate the CPU. For some specialized applications, you may need to
increase an application’s priority class, but usually you won’t. As
mentioned, neither of the applications in this chapter changes the
priority class.
In the event that you do want to change the priority class of a
program, you can do by calling SetPriorityClass( ). You can obtain the
current priority class by calling GetPriorityClass( ). The prototypes
for these functions are shown here:
DWORD GetPriorityClass(HANDLE hApp);
BOOL SetPriorityClass(HANDLE hApp, DWORD priority);
Here, hApp is the handle of the process. GetPriorityClass(
) returns the priority class of the application or zero on failure. For
SetPriorityClass( ), priority specifies the process’s new priority class.
Thread Priorities
For any given priority class, each individual thread’s priority
determines how much CPU time it receives within its process. When a
thread is first created, it is given normal priority, but you can
change a thread’s priority—even while it is executing.
You can obtain a thread’s priority setting by calling
GetThreadPriority( ). You can increase or decrease a thread’s priority
using SetThreadPriority( ). The prototypes for these functions are
shown here:
BOOL SetThreadPriority(HANDLE hThread, int priority);
int GetThreadPriority(HANDLE hThread);
For both functions, hThread is the handle of the thread. For SetThreadPriority( ), priority
is the new priority setting. If an error occurs, SetThreadPriority( )
returns zero. It returns nonzero otherwise. For GetThreadPriority( ),
the current priority setting is returned. The priority settings are
shown here, in order of highest to lowest:
|
Thread Priority |
Value |
|
THREAD_PRIORITY_TIME_CRITICAL |
15 |
|
THREAD_PRIORITY_HIGHEST |
2 |
|
THREAD_PRIORITY_ABOVE_NORMAL |
1 |
|
THREAD_PRIORITY_NORMAL |
0 |
|
THREAD_PRIORITY_BELOW_NORMAL |
-1 |
|
THREAD_PRIORITY LOWEST |
-2 |
|
THREAD_PRIORITY_IDLE |
-15 |
These values are increments or decrements that are applied relative
to the priority class of the process. Through the combination of a
process’ priority class and thread priority, Windows supports 31
different priority settings for application programs.
GetThreadPriority( ) returns THREAD_PRIORITY_ERROR_RETURN if an error occurs.
For the most part, if a thread has the NORMAL_PRIORITY class, you
can freely experiment with changing its priority setting without fear
of catastrophically affecting overall system performance. As you will
see, the thread control panel developed in the next section allows you
to alter the priority setting of a thread within a process (but does
not change its priority class).
Obtaining the Handle of the Main Thread
It is possible to control the execution of the main thread. To do
so, you will need to acquire its handle. The easiest way to do this is
to call GetCurrentThread( ), whose prototype is shown here:
HANDLE GetCurrentThread(void);
This function returns a pseudohandle to the current thread. It is
called a pseudohandle because it is a predefined value that always
refers to the current thread rather than specifically to the calling
thread. It can, however, be used any place that a normal thread handle
can.
Synchronization
When using multiple threads or processes, it is sometimes necessary
to coordinate the activities of two or more. This process is called synchronization.
The most common use of synchronization occurs when two or more threads
need access to a shared resource that must be used by only one thread
at a time. For example, when one thread is writing to a file, a second
thread must be prevented from doing so at the same time. Another reason
for synchronization is when one thread is waiting for an event that is
caused by another thread. In this case, there must be some means by
which the first thread is held in a suspended state until the event has
occurred. Then the waiting thread must resume execution.
There are two general states that a task may be in. First, it may be executing (or ready to execute as soon as it obtains its time slice). Second, a task may be blocked, awaiting some resource or event, in which case its execution is suspended until the needed resource is available or the event occurs.
If you are not familiar with the synchronization problem or its most
common solution, the semaphore, the next section discusses it.
Understanding the Synchronization Problem
Windows must provide special services that allow access to a
shared resource to be synchronized, because without help from the
operating system, there is no way for one process or thread to know
that it has sole access to a resource. To understand this, imagine that
you are writing programs for a multitasking operating system that does
not provide any synchronization support. Further imagine that you have
two concurrently executing threads, A and B, both of which, from time
to time, require access to some resource R (such as a disk file) that
must be accessed by only one thread at a time. As a means of preventing
one thread from accessing R while the other is using it, you try the
following solution. First, you establish a variable called flag that is
initialized to zero and can be accessed by both threads. Then, before
using each piece of code that accesses R, you wait for flag to be
cleared, then set flag, access R, and finally, clear flag. That is,
before either thread accesses R, it executes this piece of code:
while(flag) ; // wait for flag to be cleared
flag = 1; // set flag
// ... access resource R ...
flag = 0; // clear the flag
The idea behind this code is that neither thread will access R if
flag is set. Conceptually, this approach is in the spirit of the
correct solution. However, in actual fact it leaves much to be desired
for one simple reason: it won’t always work! Let’s see why.
Using the code just given, it is possible for both processes to
access R at the same time. The while loop is, in essence, performing
repeated load and compare instructions on flag or, in other words, it
is testing flag’s value. When flag is cleared, the next line of code
sets flag’s value. The trouble is that it is possible for these two
operations to be performed in two different time slices. Between the
two time slices, the value of flag might have been accessed by the
other thread, thus allowing R to be used by both threads at the same
time. To understand this, imagine that thread A enters the while loop
and finds that flag is zero, which is the green light to access R.
However, before it can set flag to 1, its time slice expires and thread
B resumes execution. If B executes its while, it too will find that
flag is not set and assume that it is safe to access R. However, when A
resumes it will also begin accessing R. The crucial aspect of the
problem is that the testing and setting of flag do not comprise one
uninterruptible operation. Rather, as just illustrated, they can be
separated by a time slice. No matter how you try, there is no way,
using only application-level code, that you can absolutely guarantee
that one and only one thread will access R at one time.
The solution to the synchronization problem is as elegant as it is
simple. The operating system (in this case Windows) provides a routine
that in one uninterrupted operation, tests and, if possible, sets a
flag. In the language of operating systems engineers, this is called a test and set
operation. For historical reasons, the flags used to control access to
a shared resource and provide synchronization between threads (and
processes) are called semaphores. The semaphore is at the core of the Windows synchronization system.
Windows supports several types of synchronization objects. The first
type is the classic semaphore. When using a semaphore, a resource can
be completely synchronized, in which case one and only one thread or
process can access it at any one time, or the semaphore can allow no
more than a small number of processes or threads access at any one
time. Semaphores are implemented using a counter that is decremented
when a task is granted the semaphore and incremented when the task
releases it.
The second synchronization object is the mutex semaphore, or just mutex,
for short. A mutex synchronizes a resource such that one and only one
thread or process can access it at any one time. In essence, a mutex is
a special case version of a standard semaphore.
The third synchronization object is the event object. It
can be used to block access to a resource until some other thread or
process signals that it can be used. (That is, an event object signals
that a specified event has occurred.)
The fourth synchronization object is the waitable timer. A waitable timer blocks a thread’s execution until a specific time. You can also create timer queues, which are lists of timers.
You can prevent a section of code from being used by more than one thread at a time by making it into a critical section
using a critical section object. Once a critical section is entered by
one thread, no other thread may use it until the first thread has left
the critical section.
The only synchronization object used in this chapter is the mutex,
which is described in the following section. However, all
synchronization objects defined by Windows are available to the C++
programmer. As explained, this is one of the major advantages that
results from C++’s reliance on the operating system to handle
multithreading: all multithreading features are at your command.
Using a Mutex to Synchronize Threads
As explained, a mutex is a special-case semaphore that allows
only one thread to access a resource at any given time. Before you can
use a mutex, you must create one using CreateMutex( ), whose prototype
is shown here:
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES secAttr,
BOOL acquire,
LPCSTR name);
Here, secAttr is a pointer to the security attributes. If secAttr is NULL, the default security descriptor is used.
If the creating thread desires control of the mutex, then acquire must be true. Otherwise, pass false.
The name parameter points to a string that becomes the name
of the mutex object. Mutexes are global objects, which may be used by
other processes. As such, when two processes each open a mutex using
the same name, both are referring to the same mutex. In this way, two
processes can be synchronized. The name may also be NULL, in which case the semaphore is localized to one process.
The CreateMutex( ) function returns a handle to the semaphore if
successful or NULL on failure. A mutex handle is automatically closed
when the main process ends. You can explicitly close a mutex handle
when it is no longer needed by calling CloseHandle( ).
Once you have created a semaphore, you use it by calling two related
functions: WaitForSingleObject( ) and ReleaseMutex( ). The prototypes
for these functions are shown here:
DWORD WaitForSingleObject(HANDLE hObject, DWORD howLong);
BOOL ReleaseMutex(HANDLE hMutex);
WaitForSingleObject( ) waits on a synchronization object. It does
not return until the object becomes available or a time-out occurs. For
use with mutexes, hObject will be the handle of a mutex. The howLong
parameter specifies, in milliseconds, how long the calling routine will
wait. Once that time has elapsed, a time-out error will be returned. To
wait indefinitely, use the value INFINITE. The
function returns WAIT_OBJECT_0 when successful (that is, when access is
granted). It returns WAIT_TIMEOUT when time-out is reached.
ReleaseMutex( ) releases the mutex and allows another thread to acquire it. Here, hMutex is the handle to the mutex. The function returns nonzero if successful and zero on failure.
To use a mutex to control access to a shared resource, wrap the code
that accesses that resource between a call to WaitForSingleObject( )
and ReleaseMutex( ), as shown in this skeleton. (Of course, the
time-out period will differ from application to application.)
if(WaitForSingleObject(hMutex, 10000)==WAIT_TIMEOUT) {
// handle time-out error
}
// access the resource
ReleaseMutex(hMutex);
Generally, you will want to choose a time-out period that will be
more than enough to accommodate the actions of your program. If you get
repeated time-out errors when developing a multithreaded application,
it usually means that you have created a deadlock condition. Deadlock occurs when one thread is waiting on a mutex that another thread never releases.
Creating a Thread Control Panel
When developing multithreaded programs, it is often useful to
experiment with various priority settings. It is also useful to be able
to dynamically suspend and resume a thread, or even terminate a thread.
As you will see, it is quite easy, using the thread functions just
described, to create a thread control panel that allows you to
accomplish these things. Further, you can use the control panel while
your multithreaded program is running. The dynamic nature of the thread
control panel allows you to easily change the execution profile of a
thread and observe the results.
The thread control panel developed in this section is capable of
controlling one thread. However, you can create as many panels as
needed, with each controlling a different thread. For the sake of
simplicity, the control panel is implemented as a modeless dialog box
that is owned by the desktop, not the application whose thread it
controls.
The thread control panel is capable of performing the following actions:
- Setting a thread’s priority
- Suspending a thread
- Resuming a thread
- Terminating a thread
It also displays the current priority setting of the thread. The thread control dialog box is shown in Figure 3-1.
As stated, the control panel is as a modeless dialog box. As you
know, when a modeless dialog box is activated, the rest of the
application is still active. Thus, the control panel runs independently
of the application for which it is being used.

Figure 3-1. The Thread Control dialog box
The code for the thread control panel is shown here. This file is called tcp.cpp.
// A thread control panel.
#include <map>
#include <windows.h>
#include "panel.h"
using namespace std;
const int NUMPRIORITIES = 5;
const int OFFSET = 2;
// Array of strings for priority list box.
char priorities[NUMPRIORITIES][80] = {
"Lowest",
"Below Normal",
"Normal",
"Above Normal",
"Highest"
};
// A Thread Control Panel Class.
class ThrdCtrlPanel {
// Information about the thread under control.
struct ThreadInfo {
HANDLE hThread; // handle of thread
int priority; // current priority
bool suspended; // true if suspended
ThreadInfo(HANDLE ht, int p, bool s) {
hThread = ht;
priority = p;
suspended = s;
}
};
// This map holds a ThreadInfo for each
// active thread control panel.
static map<HWND, ThreadInfo> dialogmap;
public:
// Construct a control panel.
ThrdCtrlPanel(HINSTANCE hInst, HANDLE hThrd);
// The control panel's callback function.
static LRESULT CALLBACK ThreadPanel(HWND hwnd, UINT message,
WPARAM wParam, LPARAM lParam);
};
// Define static member dialogmap.
map<HWND, ThrdCtrlPanel::ThreadInfo>
ThrdCtrlPanel::dialogmap;
// Create a thread control panel. ThrdCtrlPanel::ThrdCtrlPanel(HINSTANCE hInst,
HANDLE hThrd)
{
ThreadInfo ti(hThrd,
GetThreadPriority(hThrd)+OFFSET,
false);
// Owner window is desktop.
HWND hDialog = CreateDialog(hInst, "ThreadPanelDB",
NULL,
(DLGPROC) ThreadPanel);
// Put info about this dialog box in the map.
dialogmap.insert(pair<HWND, ThreadInfo>(hDialog, ti));
// Set the control panel's title.
char str[80] = "Control Panel for Thread ";
char str2[4];
_itoa(dialogmap.size(), str2, 10);
strcat(str, str2);
SetWindowText(hDialog, str);
// Offset each dialog box instance.
MoveWindow(hDialog, 30*dialogmap.size(),
30*dialogmap.size(),
300, 250, 1);
// Update priority setting in the list box.
SendDlgItemMessage(hDialog, IDD_LB, LB_SETCURSEL,
(WPARAM) ti.priority, 0);
// Increase priority to ensure control. You can
// change or remove this statement based on your
// execution environment.
SetThreadPriority(GetCurrentThread(),
THREAD_PRIORITY_ABOVE_NORMAL);
}
// Thread control panel dialog box callback function.
LRESULT CALLBACK ThrdCtrlPanel::ThreadPanel(HWND hwnd,
UINT message,
WPARAM wParam,
LPARAM lParam)
{
int i;
HWND hpbRes, hpbSus, hpbTerm;
switch(message) {
case WM_INITDIALOG:
// Initialize priority list box.
for(i=0; i<NUMPRIORITIES i++) {
SendDlgItemMessage(hwnd, IDD_LB,
LB_ADDSTRING, 0, (LPARAM) priorities[i]);
}
// Set suspend and resume buttons for thread.
hpbSus = GetDlgItem(hwnd, IDD_SUSPEND);
hpbRes = GetDlgItem(hwnd, IDD_RESUME);
EnableWindow(hpbSus, true); // enable Suspend
EnableWindow(hpbRes, false); // disable Resume
return 1;
case WM_COMMAND:
map<HWND, ThreadInfo>::iterator p = dialogmap.find(hwnd);
switch(LOWORD(wParam)) {
case IDD_TERMINATE:
TerminateThread(p->second.hThread, 0);
// Disable Terminate button.
hpbTerm = GetDlgItem(hwnd, IDD_TERMINATE); }
EnableWindow(hpbTerm, false); // disable
// Disable Suspend and Resume buttons.
hpbSus = GetDlgItem(hwnd, IDD_SUSPEND);
hpbRes = GetDlgItem(hwnd, IDD_RESUME);
EnableWindow(hpbSus, false); // disable Suspend
EnableWindow(hpbRes, false); // disable Resume
return 1;
case IDD_SUSPEND:
SuspendThread(p->second.hThread);
// Set state of the Suspend and Resume buttons.
hpbSus = GetDlgItem(hwnd, IDD_SUSPEND);
hpbRes = GetDlgItem(hwnd, IDD_RESUME);
EnableWindow(hpbSus, false); // disable Suspend
EnableWindow(hpbRes, true); // enable Resume
p->second.suspended = true;
return 1;
case IDD_RESUME:
ResumeThread(p->second.hThread);
// Set state of the Suspend and Resume buttons.
hpbSus = GetDlgItem(hwnd, IDD_SUSPEND);
hpbRes = GetDlgItem(hwnd, IDD_RESUME); /'
EnableWindow(hpbSus, true); // enable Suspend /'
EnableWindow(hpbRes, false); // disable Resume
p->second.suspended = false;
return 1;
case IDD_LB:
// If a list box entry was clicked,
// then change the priority.
if(HIWORD(wParam)==LBN_DBLCLK) {
p->second.priority = SendDlgItemMessage(hwnd,
IDD_LB, LB_GETCURSEL,/'
0, 0);
SetThreadPriority(p->second.hThread,
p->second.priority-OFFSET);
}
return 1;
case IDCANCEL:
// If thread is suspended when panel is closed,
// then resume thread to prevent deadlock.
if(p->second.suspended) {
ResumeThread(p->second.hThread);
p->second.suspended = false;
}
// Remove this thread from the list.
dialogmap.erase(hwnd);
// Close the panel.
DestroyWindow(hwnd);?
return 1;
}
}
return 0;
}
The control panel requires the following resource file, called tcp.rc:
#include <windows.h>
#include "panel.h"
ThreadPanelDB DIALOGEX 20, 20, 140, 110
CAPTION "Thread Control Panel"
STYLE WS_BORDER | WS_VISIBLE | WS_POPUP | WS_CAPTION | WS_SYSMENU
{
DEFPUSHBUTTON "Done", IDCANCEL, 55, 80, 33, 14
PUSHBUTTON "Terminate", IDD_TERMINATE, 10, 20, 42, 12
PUSHBUTTON "Suspend", IDD_SUSPEND, 10, 35, 42, 12
PUSHBUTTON "Resume", IDD_RESUME, 10, 50, 42, 12
LISTBOX IDD_LB, 65, 20, 63, 42, LBS_NOTIFY | WS_VISIBLE |
WS_BORDER | WS_VSCROLL | WS_TABSTOP
CTEXT "Thread Priority", IDD_TEXT1, 65, 8, 64, 10
CTEXT "Change State", IDD_TEXT2, 0, 8, 64, 10
}
The control panel uses the following header file called panel.h:
#define IDD_LB 200
#define IDD_TERMINATE 202
#define IDD_SUSPEND 204
#define IDD_RESUME 206
#define IDD_TEXT1 208
#define IDD_TEXT2 209
To use the thread control panel, follow these steps:
- Include tcp.cpp in your program.
- Include tcp.rc in your program’s resource file.
- Create the thread or threads that you want to control.
- Instantiate a ThrdCtrlPanel object for each thread.
Each ThrdCtrlPanel object links a thread with a dialog box that
controls it. For large projects in which multiple files need access to
ThrdCtrlPanel, you will need to use a header file called tcp.h that
contains the declaration for ThrdCtrlPanel. Here is tcp.h:
// A header file for the ThrdCtrlPanel class.
class ThrdCtrlPanel {
public:
// Construct a control panel.
ThrdCtrlPanel(HINSTANCE hInst, HANDLE hThrd);
// The control panel's callback function.
static LRESULT CALLBACK ThreadPanel(HWND hwnd, UINT message,
WPARAM wParam, LPARAM lParam);
};
Let’s take a closer look at the thread control panel. It begins by defining the following global definitions:
const int NUMPRIORITIES = 5;
const int OFFSET = 2;
// Array of strings for priority list box.
char priorities[NUMPRIORITIES][80] = {
"Lowest",
"Below Normal",
"Normal",
"Above Normal",
"Highest"
};
The priorities array holds strings that correspond to a thread’s
priority setting. It initializes the list box inside the control panel
that displays the current thread priority. The number of priorities is
specified by NUMPRIORITIES, which is 5 for Windows. Thus, NUMPRIORITIES
defines the number of different priorities that a thread may have. (If
you adapt the code for use with another operating system, a different
value might be required.) Using the control panel, you can set a thread
to one of the following priorities:
THREAD_PRIORITY_HIGHEST
THREAD_PRIORITY_ABOVE_NORMAL
THREAD_PRIORITY_NORMAL
THREAD_PRIORITY_BELOW_NORMAL
THREAD_PRIORITY_LOWEST
The other two thread priority settings:
THREAD_PRIORITY_TIME_CRITICAL
THREAD_PRIORITY_IDLE
are not supported because, relative to the control panel, they are
of little practical value. For example, if you want to create a
time-critical application, you are better off making its priority class
time-critical.
OFFSET defines an offset that will be used to translate between list
box indexes and thread priorities. You should recall that normal
priority has the value zero. In this example, the highest priority is
THREAD_PRIORITY_HIGHEST, which is 2. The lowest priority is
THREAD_PRIORITY_LOWEST, which is –2. Because list box indexes begin at
zero, the offset is used to convert between indexes and priority
settings.
Next, the ThrdCtrlPanel class is declared. It begins as shown here:
// A Thread Control Panel Class.
class ThrdCtrlPanel {
// Information about the thread under control.
struct ThreadInfo {
HANDLE hThread; // handle of thread
int priority; // current priority
bool suspended; // true if suspended
ThreadInfo(HANDLE ht, int p, bool s) {
hThread = ht;
priority = p;
suspended = s;
}
};
// This map holds a ThreadInfo for each
// active thread control panel.
static map<HWND, ThreadInfo> dialogmap;
Information about the thread under control is contained within a
structure of type ThreadInfo. The handle of the thread is stored in
hThread. Its priority is stored in priority. If the thread is
suspended, then suspended will be true. Otherwise, suspended will be
false.
The static member dialogmap is an STL map that links the thread
information with the handle of the dialog box used to control that
thread. Because there can be more than one thread control panel active
at any given time, there must be some way to determine which thread is
associated with which panel. It is dialogmap that provides this linkage.
The ThreadCtrlPanel Constructor
The ThrdCtrlPanel constructor is shown here. The constructor is
passed the instance handle of the application and the handle of the
thread being controlled. The instance handle is needed to create the
control panel dialog box.
// Create a thread control panel. ThrdCtrlPanel::ThrdCtrlPanel(HINSTANCE hInst,
HANDLE hThrd)
{
ThreadInfo ti(hThrd,
GetThreadPriority(hThrd)+OFFSET,
false);
// Owner window is desktop.
HWND hDialog = CreateDialog(hInst, "ThreadPanelDB",
NULL,
(DLGPROC) ThreadPanel);
// Put info about this dialog box in the map.
dialogmap.insert(pair<HWND, ThreadInfo>(hDialog, ti));
// Set the control panel's title.
char str[80] = "Control Panel for Thread ";
char str2[4];
_itoa(dialogmap.size(), str2, 10);
strcat(str, str2);
SetWindowText(hDialog, str);
// Offset each dialog box instance.
MoveWindow(hDialog, 30*dialogmap.size(),
30*dialogmap.size(),
300, 250, 1);
// Update priority setting in the list box.
SendDlgItemMessage(hDialog, IDD_LB, LB_SETCURSEL,
(WPARAM) ti.priority, 0);
// Increase priority to ensure control. You can
// change or remove this statement based on your
// execution environment.
SetThreadPriority(GetCurrentThread(),
THREAD_PRIORITY_ABOVE_NORMAL);
}
The constructor begins by creating a ThreadInfo instance called ti
that contains the initial settings for the thread. Notice that the
priority is obtained by calling GetThreadPriority( ) for the thread
being controlled. Next, the control panel dialog box is created by
calling CreateDialog( ). CreateDialog( ) is a Windows API function that
creates a modeless dialog box, which makes it independent of the
application that creates it. The handle of this dialog box is returned
and stored in hDialog. Next, hDialog and the thread information
contained in ti are stored in dialogmap. Thus, the thread is linked
with the dialog box that controls it.
Next, the title of the dialog box is set to reflect the number of
the thread. The number of the thread is obtained based on the number of
entries in dialogmap. An alternative that you might want to try
implementing is to explicitly pass a name for each thread to the
ThrdCtrlPanel constructor. For the purposes of this chapter, simply
numbering each thread is sufficient.
Next, the control panel’s position on the screen is offset a bit by
calling MoveWindow( ), another Windows API function. This enables
multiple panels to be displayed without each one fully covering the one
before it. The thread’s priority setting is then displayed in the
priority list box by calling the Windows API function
SendDlgItemMessage( ).
Finally, the current thread has its priority increased to above
normal. This ensures that the application receives enough CPU time to
be responsive to user input no matter what is the priority level of the
thread under control. This step may not be needed in all cases. You can
experiment to find out.
The ThreadPanel( ) Function
ThreadPanel( ) is the Windows callback function that responds to
user interaction with the thread control panel. Like all dialog box
callback functions, it receives a message each time the user changes
the state of a control. It is passed the handle of the dialog box in
which the action occurred, the message, and any additional information
required by the message. Its general mode of operation is the same as
that for any other callback function used by a dialog box. The
following discussion describes what happens for each message.
When the thread control panel dialog box is first created, it
receives a WM_INITDIALOG message, which is handled by this case
sequence:
caseWM_INITDIALOG:
// Initialize priority list box.
for(i=0; i<NUMPRIORITIES i++) {
SendDlgItemMessage(hwnd, IDD_LB,
LB_ADDSTRING, 0, (LPARAM) priorities[i]);
}
// Set Suspend and Resume buttons for thread.
hpbSus = GetDlgItem(hwnd, IDD_SUSPEND);
hpbRes = GetDlgItem(hwnd, IDD_RESUME);
EnableWindow(hpbSus, true); // enable Suspend
EnableWindow(hpbRes, false); // disable Resume
return 1;
This initializes the priority list box and sets the Suspend and
Resume buttons to their initial states, which are Suspend enabled and
Resume disabled.
Each user interaction generates a WM_COMMAND message. Each time this
message is received, an iterator to this dialog box’s entry in
dialogmap is retrieved, as shown here:
case WM_COMMAND:
map<HWND, ThreadInfo>::iterator p = dialogmap.find(hwnd);
The information pointed to by p will be used to properly process
each action. Because p is an iterator for a map, it points to an object
of type pair, which is a structure defined by the STL. This structure
contains two fields: first and second. These fields correspond to the
information that comprises the key and the value, respectively. In this
case, the handle is the key and the thread information is the value.
A code indicating precisely what action has occurred is contained in
the low-order word of wParam, which is used to control a switch
statement that handles the remaining messages. Each is described next.
When the user presses the Terminate button, the thread under control is stopped. This is handled by this case sequence:
case IDD_TERMINATE:
TerminateThread(p->second.hThread, 0);
// Disable Terminate button.
hpbTerm = GetDlgItem(hwnd, IDD_TERMINATE);
EnableWindow(hpbTerm, false); // disable
// Disable Suspend and Resume buttons.
hpbSus = GetDlgItem(hwnd, IDD_SUSPEND);
hpbRes = GetDlgItem(hwnd, IDD_RESUME);
EnableWindow(hpbSus, false); // disable Suspend
EnableWindow(hpbRes, false); // disable Resume
return 1;
The thread is stopped with a call to TerminateThread( ). Notice how
the handle for the thread is obtained. As explained, because p is an
iterator for a map, it points to an object of type pair that contains
the key in its first field and the value in its second field. This is
why the thread handle is obtained by the expression
p->second.hThread. After the thread is stopped, the Terminate button
is disabled.
Once a thread has been terminated, it cannot be resumed. Notice that
the control panel uses TerminateThread( ) to halt execution of a
thread. As mentioned earlier, this function must be used with care. If
you use the control panel to experiment with threads of your own, you
will want to make sure that no harmful side effects are possible.
When the user presses the Suspend button, the thread is suspended. This is accomplished by the following sequence:
case IDD_SUSPEND:
SuspendThread(p->second.hThread);
// Set state of the Suspend and Resume buttons.
hpbSus = GetDlgItem(hwnd, IDD_SUSPEND);
hpbRes = GetDlgItem(hwnd, IDD_RESUME);
EnableWindow(hpbSus, false); // disable Suspend
EnableWindow(hpbRes, true); // enable Resume
p->second.suspended = true;
return 1;
The thread is suspended by a call to SuspendThread( ). Next, the
state of the Suspend and Resume buttons are updated such that Resume is
enabled and Suspend is disabled. This prevents the user from attempting
to suspend a thread twice.
A suspended thread is resumed when the Resume button is pressed. It is handled by this code:
case IDD_RESUME:
ResumeThread(p->second.hThread);
// Set state of the Suspend and Resume buttons.
hpbSus = GetDlgItem(hwnd, IDD_SUSPEND);
hpbRes = GetDlgItem(hwnd, IDD_RESUME);
EnableWindow(hpbSus, true); // enable Suspend
EnableWindow(hpbRes, false); // disable Resume
p->second.suspended = false;
return 1;
The thread is resumed by a call to ResumeThread( ), and the Suspend and Resume buttons are set appropriately.
To change a thread’s priority, the user double-clicks an entry in the Priority list box. This event is handled as shown next:
case IDD_LB:
// If a list box entry was double-clicked,
// then change the priority.
if(HIWORD(wParam)==LBN_DBLCLK) {
p->second.priority = SendDlgItemMessage(hwnd,
IDD_LB, LB_GETCURSEL,
0, 0);
SetThreadPriority(p->second.hThread,
p->second.priority-OFFSET);
}
return 1;
List boxes generate various types of notification messages that
describe the precise type of event that occurred. Notification messages
are contained in the high-order word of wParam. One of these messages
is LBN_DBLCLK, which means that the user double-clicked an entry in the
box. When this notification is received, the index of the entry is
retrieved by calling the Windows API function SendDlgItemMessage( ),
requesting the current selection. This value is then used to set the
thread’s priority. Notice that OFFSET is subtracted to normalize the
value of the index.
Finally, when the user closes the thread control panel dialog box,
the IDCANCEL message is sent. It is handled by the following sequence:
case IDCANCEL:
// If thread is suspended when panel is closed,
// then resume thread to prevent deadlock.
if(p->second.suspended) {
ResumeThread(p->second.hThread);
p->second.suspended = false;
}
// Remove this thread from the list.
dialogmap.erase(hwnd);
// Close the panel.
DestroyWindow(hwnd);
return 1;
If the thread was suspended, it is restarted. This is necessary to
avoid accidentally deadlocking the thread. Next, this dialog box’s
entry in dialogmap is removed. Finally, the dialog box is removed by
calling the Windows API function DestroyWindow( ).
Here is a program that includes the thread control panel and
demonstrates its use. Sample output is shown in Figure 3-2. The program
creates a main window and defines two child threads. When started,
these threads simply count from 0 to 50,000, displaying the count in
the main window. These threads can be controlled by activating a thread
control panel.
To use the program, first begin execution of the threads by
selecting Start Threads from the Threads menu (or by pressing F2) and
then activate the thread control panels by selecting Control Panels
from the Threads menu (or by pressing F3). Once the control panels are
active, you can experiment with different priority settings and so on.

Figure 3-2. Sample output from the thread control panel sample program
NOTE
It is beyond the scope of this book to teach Windows programming.
However, the operation of this sample program is straightforward and
should be easily understood by all Windows programmers.
// Demonstrate the thread control panel.
#include <windows.h>
#include <process.h>
#include "thrdapp.h"
#include "tcp.cpp"
const int MAX = 500000;
LRESULT CALLBACK WindowFunc(HWND, UINT, WPARAM, LPARAM);
unsigned __stdcall MyThread1(void * param);
unsigned __stdcall MyThread2(void * param);
char str[255]; // holds output strings
unsigned tid1, tid2; // thread IDs
HANDLE hThread1, hThread2; // thread handles
HINSTANCE hInst; // instance handle
int WINAPI WinMain(HINSTANCE hThisInst, HINSTANCE hPrevInst,
LPSTR args, int winMode)
{
HWND hwnd;
MSG msg;
WNDCLASSEX wcl;
HACCEL hAccel;
// Define a window class.
wcl.cbSize = sizeof(WNDCLASSEX);
wcl.hInstance = hThisInst; // handle to this instance
wcl.lpszClassName = "MyWin"; // window class name
wcl.lpfnWndProc = WindowFunc; // window function
wcl.style = 0; // default style
wcl.hIcon = LoadIcon(NULL, IDI_APPLICATION); // large icon
wcl.hIconSm = NULL; // use small version of large icon
wcl.hCursor = LoadCursor(NULL, IDC_ARROW); // cursor style
wcl.lpszMenuName = "ThreadAppMenu"; // main menu
wcl.cbClsExtra = 0; // no extra memory needed
wcl.cbWndExtra = 0;
// Make the window background white.
wcl.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH);
// Register the window class.
if(!RegisterClassEx(&wcl)) return 0;
/* Now that a window class has been registered, a window
can be created. */
hwnd = CreateWindow(
wcl.lpszClassName, // name of window class
"Using a Thread Control Panel", // title
WS_OVERLAPPEDWINDOW, // window style - normal
CW_USEDEFAULT, // X coordinate - let Windows decide
CW_USEDEFAULT, // Y coordinate - let Windows decide
260, // width
200, // height
NULL, // no parent window
NULL, // no override of class menu
hThisInst, // instance handle
NULL // no additional arguments
);
hInst = hThisInst; // save instance handle
// Load the keyboard accelerators.
hAccel = LoadAccelerators(hThisInst, "ThreadAppMenu");
// Display the window.
ShowWindow(hwnd, winMode);
UpdateWindow(hwnd);
// Create the message loop.
while(GetMessage(&msg, NULL, 0, 0))
{
if(!TranslateAccelerator(hwnd, hAccel, &msg)) {
TranslateMessage(&msg); // translate keyboard messages
DispatchMessage(&msg); // return control to Windows
}
}
return msg.wParam;
}
/* This function is called by Windows and is passed
messages from the message queue.
*/
LRESULT CALLBACK WindowFunc(HWND hwnd, UINT message,
WPARAM wParam, LPARAM lParam)
{
int response;
switch(message) {
case WM_COMMAND:
switch(LOWORD(wParam)) {
case IDM_THREAD: // create the threads
hThread1 = (HANDLE) _beginthreadex(NULL, 0,
MyThread1, (void *) hwnd,
0, &tid1);
hThread2 = (HANDLE) _beginthreadex(NULL, 0,
MyThread2, (void *) hwnd,
0, &tid2);
break;
case IDM_PANEL: // activate control panel
ThrdCtrlPanel(hInst, hThread1);
ThrdCtrlPanel(hInst, hThread2);
break;
case IDM_EXIT:
response = MessageBox(hwnd, "Quit the Program?",
"Exit", MB_YESNO);
if(response == IDYES) PostQuitMessage(0);
break;
case IDM_HELP:
MessageBox(hwnd,
"F1: Help\nF2: Start Threads\nF3: Panel",
"Help", MB_OK);
break;
}
break;
case WM_DESTROY: // terminate the program
PostQuitMessage(0);
break;
default:
return DefWindowProc(hwnd, message, wParam, lParam);
}
return 0;
}
// First thread.
unsigned __stdcall MyThread1(void * param)
{
int i;
HDC hdc;
for(i=0; i<MAX; i++) {
wsprintf(str, "Thread 1: loop # %5d ", i);
hdc = GetDC((HWND) param);
TextOut(hdc, 1, 1, str, lstrlen(str));
ReleaseDC((HWND) param, hdc);
}
return 0;
}
// Second thread.
unsigned __stdcall MyThread2(void * param)
{
int i;
HDC hdc;
for(i=0; i<MAX; i++) {
wsprintf(str, "Thread 2: loop # %5d ", i);
hdc = GetDC((HWND) param);
TextOut(hdc, 1, 20, str, lstrlen(str));
ReleaseDC((HWND) param, hdc);
}
return 0;
}
This program requires the header file thrdapp.h, shown here:
#define IDM_THREAD 100
#define IDM_HELP 101
#define IDM_PANEL 102
#define IDM_EXIT 103
The resource file required by the program is shown here:
#include <windows.h>
#include "thrdapp.h"
#include "tcp.rc"
ThreadAppMenu MENU
{
POPUP "&Threads" {
MENUITEM "&Start Threads\tF2", IDM_THREAD
MENUITEM "&Control Panels\tF3", IDM_PANEL
MENUITEM "E&xit\tCtrl+X", IDM_EXIT
}
MENUITEM "&Help", IDM_HELP
}
ThreadAppMenu ACCELERATORS
{
VK_F1, IDM_HELP, VIRTKEY
VK_F2, IDM_THREAD, VIRTKEY
VK_F3, IDM_PANEL, VIRTKEY
"^X", IDM_EXIT
}
Although controlling threads using the thread control panel is
useful when developing multithreaded programs, ultimately it is using
threads that makes them important. Toward this end, this chapter shows
a multithreaded version of the GCPtr garbage collector class originally
developed in Chapter 2. Recall that the version of GCPtr shown in
Chapter 2 collected unused memory each time a GCPtr object went out of
scope. Although this approach is fine for some applications, often a
better alternative is have the garbage collector run as a background
task, recycling memory whenever free CPU cycles are available. The
implementation developed here is designed for Windows, but the same
basic techniques apply to other multithreaded environments.
To convert GCPtr into a background task is actually fairly easy, but
it does involve a number of changes. Here are the main ones:
- Member variables that support the thread must be added to GCPtr.
These variables include the thread handle, the mutex handle, and an
instance counter that keeps track of the number of GCPtr objects in
existence.
- The constructor for GCPtr must begin the garbage collection
thread. The constructor must also create the mutex that controls
synchronization. This must happen only once, when the first GCPtr
object is created.
- Another exception must be defined that will be used to indicate a time-out condition.
- The GCPtr destructor must no longer call collect( ). Garbage collection is handled by the garbage collection thread.
- A function called gc( ) that serves as the thread entry point for the garbage collector must be defined.
- A function called isRunning( ) must be defined. It returns true if the garbage collection is in use.
- The member functions of GCPtr that access the garbage
collection list contained in gclist must be synchronized so that only
one thread at a time can access the list.
The following sections show the changes.
The Additional Member Variables
The multithreaded version of GCPtr requires that the following member variables be added:
// These support multithreading.
unsigned tid; // thread id
static HANDLE hThrd; // thread handle
static HANDLE hMutex; // handle of mutex
static int instCount; // counter of GCPtr objects
The ID of the thread used by the garbage collector is stored in tid.
This member is unused except in the call to _beginthreadex( ). The
handle to the thread is stored in hThrd. The handle of the mutex used
to synchronize access to GCPtr is stored in hMutex. A count of GCPtr
objects in existence is maintained in instCount. The last three are
static because they are shared by all instances of GCPtr. They are
defined like this, outside of GCPtr:
template <class T, int size>
int GCPtr<T, size>::instCount = 0;
template <class T, int size>
HANDLE GCPtr<T, size>::hMutex = 0;
template <class T, int size>
HANDLE GCPtr<T, size>::hThrd = 0;
The Multithreaded GCPtr Constructor
In addition to its original duties, the multithreaded GCPtr( ) must
create the mutex, start the garbage collector thread, and update the
instance counter. Here is the updated version:
// Construct both initialized and uninitialized objects. GCPtr(T *t=NULL) {
// When first object is created, create the mutex
// and register shutdown().
if(hMutex==0) {
hMutex = CreateMutex(NULL, 0, NULL);
atexit(shutdown);
}
if(WaitForSingleObject(hMutex, 10000)==WAIT_TIMEOUT)
throw TimeOutExc();
list<GCInfo<T> >::iterator p;
p = findPtrInfo(t);
// If t is already in gclist, then
// increment its reference count.
// Otherwise, add it to the list.
if(p != gclist.end())
p->refcount++; // increment ref count
else {
// Create and store this entry.
GCInfo<T> gcObj(t, size);
gclist.push_front(gcObj);
}
addr = t;
arraySize = size;
if(size > 0) isArray = true;
else isArray = false;
// Increment instance counter for each new object.
instCount++;
// If the garbage collection thread is not
// currently running, start it running.
if(hThrd==0) {
hThrd = (HANDLE) _beginthreadex(NULL, 0, gc,
(void *) 0, 0, (unsigned *) &tid);
// For some applications, it will be better
// to lower the priority of the garbage collector
// as shown here:
//
// SetThreadPriority(hThrd,
// THREAD_PRIORITY_BELOW_NORMAL);
}
ReleaseMutex(hMutex);
}
Let’s examine this code closely. First, if hMutex is zero, it means
that this is the first GCPtr object to be created and no mutex has yet
been created for the garbage collector. If this is the case, the mutex
is created and its handle is assigned to hMutex. At the same time, the
function shutdown( ) is registered as a termination function by calling
atexit( ).
It is important to note that in the multithreaded garbage collector,
shutdown( ) serves two purposes. First, as in the original version of
GCPtr, shutdown( ) frees any unused memory that has not been released
because of a circular reference. Second, when a program using the
multithreaded garbage collector ends, it stops the garbage collection
thread. This means that there might still be dynamically allocated
objects that haven’t been freed. This is important because these
objects might have destructors that need to be called. Because
shutdown( ) releases all remaining objects, it also releases these
objects.
Next, the mutex is acquired by calling WaitForSingleObject( ). This
is necessary to prevent two threads from accessing gclist at the same
time. Once the mutex has been acquired, a search of gclist is made,
looking for any preexisting entry that matches the address in t. If one
is found, its reference count is incremented. If no preexising entry
matches t, a new GCInfo object is created that contains this address,
and this object is added to gclist. Then, addr, arraySize, and isArray
are set. These actions are the same as in the original version of GCPtr.
Next, instCount is incremented. Recall that instCount is initialized
to zero. Incrementing it each time an object is created keeps track of
how many GCPtr objects are in existence. As long as this count is above
zero, the garbage collector will continue to execute.
Next, if hThrd is zero (as it is initially), then no thread has yet
been created for the garbage collector. In this case, _beginthreadex( )
is called to begin the thread. A handle to the thread is then assigned
to hThrd. The thread entry function is called gc( ), and it is examined
shortly.
Finally, the mutex is released and the constructor returns. It is
important to point out that each call to WaitForSingleObject( ) must be
balanced by a call to ReleaseMutex( ), as shown in the GCPtr
constructor. Failure to release the mutex will cause deadlock.
The TimeOutExc Exception
As you probably noticed in the code for GCPtr( ) described in the
preceding section, if the mutex cannot be acquired after 10 seconds,
then a TimeOutExc is thrown. Frankly, 10 seconds is a very long time,
so a time-out shouldn’t ever happen unless something disrupts the task
scheduler of the operating system. However, in the event it does occur,
your application code may want to catch this exception. The TimeOutExc
class is shown here:
// Exception thrown when a time-out occurs
// when waiting for access to hMutex.
//
class TimeOutExc {
// Add functionality if needed by your application.
};
Notice that it contains no members. Its existence as a unique type
is sufficient for the purposes of this chapter. Of course, you can add
functionality if desired.
The Multithreaded GCPtr Destructor
Unlike the single-threaded version of the GCPtr destructor, the
multithreaded version of ~GCPtr( ) does not call collect( ). Instead,
it simply decrements the reference count of the memory pointed to by
the GCPtr that is going out of scope. The actual collection of garbage
(if any exists) is handled by the garbage collection thread. The
destructor also decrements the instance counter, instCount.
The multithreaded version of ~GCPtr( ) is shown here:
// Destructor for GCPtr.
template <class T, int size>
GCPtr<T, size>::~GCPtr() {
if(WaitForSingleObject(hMutex, 10000)==WAIT_TIMEOUT)
throw TimeOutExc();
list<GCInfo<T> >::iterator p;
p = findPtrInfo(addr);
if(p->refcount) p->refcount--; // decrement ref count
// Decrement instance counter for each object
// that is destroyed.
instCount--;
ReleaseMutex(hMutex);
}
The gc( ) Function
The entry function for the garbage collector is called gc( ), and it is shown here:
// Entry point for garbage collector thread.
template <class T, int size>
unsigned __stdcall GCPtr<T, size>::gc(void * param) {
#ifdef DISPLAY
cout << "Garbage collection started.\n";
#endif
while(isRunning()) {
collect();
}
collect(); // collect garbage on way out
// Release and reset the thread handle so
// that the garbage collection thread can
// be restarted if necessary.
CloseHandle(hThrd);
hThrd = 0;
#ifdef DISPLAY
cout << "Garbage collection terminated for "
<< typeid(T).name() << "\n";
#endif
return 0;
}
The gc( ) function is quite simple: it runs as long as the garbage
collector is in use. The isRunning( ) function returns true if
instCount is greater than zero (which means that the garbage collector
is still needed) and false otherwise. Inside the loop, collect( ) is
called continuously. This approach is suitable for demonstrating the
multithreaded garbage collector, but it is probably too inefficient for
real-world use. You might want to experiment with calling collect( )
less often, such as only when memory runs low. You could also
experiment by calling the Windows API function Sleep( ) after each call
to collect( ). Sleep( ) pauses the execution of the calling thread for
a specified number of milliseconds. While sleeping, a thread does not
consume CPU time.
When isRunning( ) returns false, the loop ends, causing gc( ) to
eventually end, which stops the garbage collection thread. Because of
the multithreading, it is possible that there will still be an entry on
gclist that has not yet been freed even though isRunning( ) returns
false. To handle this case, a final call to collect( ) is made before
gc( ) ends.
Finally, the thread handle is released via a call to the Windows API
function CloseHandle( ), and its value is set to zero. Setting hThrd to
zero enables the GCPtr constructor to restart the thread if later in
the program new GCPtr objects are created.
The isRunning( ) Function
The isRunning( ) function is shown here:
// Returns true if the collector is still in use.
static bool isRunning() { return instCount > 0; }
It simply compares instCount to zero. As long as instCount is
greater than 0, at least one GCPtr pointer is still in existence and
the garbage collector is still needed.
Many of the functions in GCPtr access gclist, which holds the
garbage collection list. Access to gclist must be synchronized to
prevent two or more threads from attempting to use it at the same time.
The reason for this is easy to understand. If access were not
synchronized, then, for example, one thread might be obtaining an
iterator to the end of the list at the same time that another thread is
adding or deleting an element from the list. In this case, the iterator
would be invalid. To prevent such problems, each sequence of code that
accesses gclist must be guarded by a mutex. The copy constructor for
GCPtr shown here is one example:
// Copy constructor.
GCPtr(const GCPtr &ob) {
if(WaitForSingleObject(hMutex, 10000)==WAIT_TIMEOUT)
throw TimeOutExc();
list<GCInfo<T> >::iterator p;
p = findPtrInfo(ob.addr);
p->refcount++; // increment ref count
addr = ob.addr;
arraySize = ob.arraySize;
if(arraySize > 0) isArray = true;
else isArray = false;
instCount++; // increase instance count for copy
ReleaseMutex(hMutex);
}
Notice that the first thing that the copy constructor does is
acquire the mutex. Once acquired, it creates a copy of the object and
adjusts the reference count for the memory being pointed to. On its way
out, the copy constructor releases the mutex. This same basic method is
applied to all functions that access gclist.
Two Other Changes
There are two other changes that you must make to the original
version of the garbage collector. First, recall that the original
version of GCPtr defined a static variable called first that indicated
when the first GCPtr was created. This variable is no longer needed
because hMutex now performs this function. Thus, remove first from
GCPtr. Because it is a static variable, you will also need to remove
its definition outside of GCPtr.
In the original, single-threaded version of the garbage collector,
if you defined the DISPLAY macro, you could watch the garbage collector
in action. Most of that code has been removed in the multithreaded
version because multithreading causes the output to be scrambled and
unintelligible in most cases. For the multithreaded version, defining
DISPLAY simply lets you know when the garbage collector has started and
when it has stopped.
The entire multithreaded version of the garbage collector is shown here. Call this file gcthrd.h.
// A garbage collector that runs as a back ground task.
#include <iostream>
#include <list>
#include <typeinfo>
#include <cstdlib>
#include <windows.h>
#include <process.h>
using namespace std;
// To watch the action of the garbage collector, define DISPLAY.
// #define DISPLAY
// Exception thrown when an attempt is made to
// use an Iter that exceeds the range of the
// underlying object.
//
class OutOfRangeExc {
// Add functionality if needed by your application.
};
// Exception thrown when a time-out occurs
// when waiting for access to hMutex.
//
class TimeOutExc {
// Add functionality if needed by your application.
};
// An iterator-like class for cycling through arrays
// that are pointed to by GCPtrs. Iter pointers
// ** do not ** participate in or affect garbage
// collection. Thus, an Iter pointing to
// some object does not prevent that object
// from being recycled.
//
template <class T> class Iter {
T *ptr; // current pointer value
T *end; // points to element one past end
T *begin; // points to start of allocated array
unsigned length; // length of sequence
public:
Iter() {
ptr = end = begin = NULL;
length = 0;
}
Iter(T *p, T *first, T *last) {
ptr = p;
end = last;
begin = first;
length = last - first;
}
// Return length of sequence to which this
// Iter points.
unsigned size() { return length; }
// Return value pointed to by ptr.
// Do not allow out-of-bounds access.
T &operator*() {
if( (ptr >= end) || (ptr < begin) )
throw OutOfRangeExc();
return *ptr;
}
// Return address contained in ptr.
// Do not allow out-of-bounds access.
T *operator->() {
if( (ptr >= end) || (ptr < begin) )
throw OutOfRangeExc();
return ptr;
}
// Prefix ++.
Iter operator++() {
ptr++;
return *this;
}
// Prefix --.
Iter operator--() {
ptr--;
return *this;
}
// Postfix ++.
Iter operator++(int notused) {
T *tmp = ptr;
ptr++;
return Iter<T>(tmp, begin, end);
}
// Postfix --.
Iter operator--(int notused) {
T *tmp = ptr;
ptr--;
return Iter<T>(tmp, begin, end);
}
// Return a reference to the object at the
// specified index. Do not allow out-of-bounds
// access.
T &operator[](int i) {
if( (i < 0) || (i >= (end-begin)) )
throw OutOfRangeExc();
return ptr[i];
}
// Define the relational operators.
bool operator==(Iter op2) {
return ptr == op2.ptr;
}
bool operator!=(Iter op2) {
return ptr != op2.ptr;
}
bool operator<(Iter op2) {
return ptr < op2.ptr;
}
bool operator<=(Iter op2) {
return ptr <= op2.ptr;
}
bool operator>(Iter op2) {
return ptr > op2.ptr;
}
bool operator>=(Iter op2) {
return ptr >= op2.ptr;
}
// Subtract an integer from an Iter.
Iter operator-(int n) {
ptr -= n;
return *this;
}
// Add an integer to an Iter.
Iter operator+(int n) {
ptr += n;
return *this;
}
// Return number of elements between two Iters.
int operator-(Iter<T> &itr2) {
return ptr - itr2.ptr;
}
};
// This class defines an element that is stored
// in the garbage collection information list.
//
template <class T> class GCInfo {
public:
unsigned refcount; // current reference count
T *memPtr; // pointer to allocated memory
/* isArray is true if memPtr points
to an allocated array. It is false
otherwise. */
bool isArray; // true if pointing to array
/* If memPtr is pointing to an allocated
array, then arraySize contains its size */
unsigned arraySize; // size of array
// Here, mPtr points to the allocated memory.
// If this is an array, then size specifies
// the size of the array.
GCInfo(T *mPtr, unsigned size=0) {
refcount = 1;
memPtr = mPtr;
if(size != 0)
isArray = true;
else
isArray = false;
arraySize = size;
}
};
// Overloading operator== allows GCInfos to be compared.
// This is needed by the STL list class.
template <class T> bool operator==(const GCInfo<T> &ob1,
const GCInfo<T> &ob2) {
return (ob1.memPtr == ob2.memPtr);
}
// GCPtr implements a pointer type that uses
// garbage collection to release unused memory.
// A GCPtr must only be used to point to memory
// that was dynamically allocated using new.
// When used to refer to an allocated array,
// specify the array size.
//
template <class T, int size=0> class GCPtr {
// gclist maintains the garbage collection list.
static list<GCInfo<T> > gclist;
// addr points to the allocated memory to which
// this GCPtr pointer currently points.
T *addr;
/* isArray is true if this GCPtr points
to an allocated array. It is false
otherwise. */
bool isArray; // true if pointing to array
// If this GCPtr is pointing to an allocated
// array, then arraySize contains its size.
unsigned arraySize; // size of the array
// These support multithreading.
unsigned tid; // thread id
static HANDLE hThrd; // thread handle
static HANDLE hMutex; // handle of mutex
static int instCount; // counter of GCPtr objects
// Return an iterator to pointer info in gclist.
typename list<GCInfo<T> >::iterator findPtrInfo(T *ptr);
public:
// Define an iterator type for GCPtr<T>.
typedef Iter<T> GCiterator;
// Construct both initialized and uninitialized objects.
GCPtr(T *t=NULL) {
// When first object is created, create the mutex
// and register shutdown().
if(hMutex==0) {
hMutex = CreateMutex(NULL, 0, NULL);
atexit(shutdown);
}
if(WaitForSingleObject(hMutex, 10000)==WAIT_TIMEOUT)
throw TimeOutExc();
list<GCInfo<T> >::iterator p;
p = findPtrInfo(t);
// If t is already in gclist, then
// increment its reference count.
// Otherwise, add it to the list.
if(p != gclist.end())
p->refcount++; // increment ref count
else {
// Create and store this entry.
GCInfo<T> gcObj(t, size);
gclist.push_front(gcObj);
}
addr = t;
arraySize = size;
if(size > 0) isArray = true;
else isArray = false;
// Increment instance counter for each new object.
instCount++;
// If the garbage collection thread is not
// currently running, start it running.
if(hThrd==0) {
hThrd = (HANDLE) _beginthreadex(NULL, 0, gc,
(void *) 0, 0, (unsigned *) &tid);
// For some applications, it will be better
// to lower the priority of the garbage collector
// as shown here:
//
// SetThreadPriority(hThrd,
// THREAD_PRIORITY_BELOW_NORMAL);
}
ReleaseMutex(hMutex);
}
// Copy constructor.
GCPtr(const GCPtr &ob) {
if(WaitForSingleObject(hMutex, 10000)==WAIT_TIMEOUT)
throw TimeOutExc();
list<GCInfo<T> >::iterator p;
p = findPtrInfo(ob.addr);
p->refcount++; // increment ref count
addr = ob.addr;
arraySize = ob.arraySize;
if(arraySize > 0) isArray = true;
else isArray = false;
instCount++; // increase instance count for copy
ReleaseMutex(hMutex);
}
// Destructor for GCPTr.
~GCPtr();
// Collect garbage. Returns true if at least
// one object was freed.
static bool collect();
// Overload assignment of pointer to GCPtr.
T *operator=(T *t);
// Overload assignment of GCPtr to GCPtr.
GCPtr &operator=(GCPtr &rv);
// Return a reference to the object pointed
// to by this GCPtr.
T &operator*() {
return *addr;
}
// Return the address being pointed to.
T *operator->() { return addr; }
// Return a reference to the object at the
// index specified by i.
T &operator[](int i) {
return addr[i];
}
// Conversion function to T *.
operator T *() { return addr; }
// Return an Iter to the start of the allocated memory. Iter<T> begin() {
int size;
if(isArray) size = arraySize;
else size = 1;
return Iter<T>(addr, addr, addr + size);
}
// Return an Iter to one past the end of an allocated array.
Iter<T> end() {
int size;
if(isArray) size = arraySize;
else size = 1;
return Iter<T>(addr + size, addr, addr + size);
}
// Return the size of gclist for this type
// of GCPtr.
static int gclistSize() {
if(WaitForSingleObject(hMutex, 10000)==WAIT_TIMEOUT)
throw TimeOutExc();
unsigned sz = gclist.size();
ReleaseMutex(hMutex);
return sz;
}
// A utility function that displays gclist.
static void showlist();
// The following functions support multithreading.
//
// Returns true if the collector is still in use.
static bool isRunning() { return instCount > 0; }
// Clear gclist when program exits.
static void shutdown();
// Entry point for garbage collector thread.
static unsigned __stdcall gc(void * param);
};
// Create storage for the static variables.
template <class T, int size>
list<GCInfo<T> > GCPtr<T, size>::gclist;
template <class T, int size>
int GCPtr<T, size>::instCount = 0;
template <class T, int size>
HANDLE GCPtr<T, size>::hMutex = 0;
template <class T, int size>
HANDLE GCPtr<T, size>::hThrd = 0;
// Destructor for GCPtr.
template <class T, int size>
GCPtr<T, size>::~GCPtr() {
if(WaitForSingleObject(hMutex, 10000)==WAIT_TIMEOUT)
throw TimeOutExc();
list<GCInfo<T> >::iterator p;
p = findPtrInfo(addr);
if(p->refcount) p->refcount--; // decrement ref count
// Decrement instance counter for each object
// that is destroyed.
instCount--;
ReleaseMutex(hMutex);
}
// Collect garbage. Returns true if at least
// one object was freed.
template <class T, int size>
bool GCPtr<T, size>::collect() {
if(WaitForSingleObject(hMutex, 10000)==WAIT_TIMEOUT)
throw TimeOutExc();
bool memfreed = false;
list<GCInfo<T> >::iterator p;
do {
// Scan gclist looking for unreferenced pointers.
for(p = gclist.begin(); p != gclist.end(); p++) {
// If in-use, skip.
if(p->refcount > 0) continue;
memfreed = true;
// Remove unused entry from gclist.
gclist.remove(*p);
// Free memory unless the GCPtr is null.
if(p->memPtr) {
if(p->isArray) {
delete[] p->memPtr; // delete array
}
else {
delete p->memPtr; // delete single element
}
}
// Restart the search.
break;
}
} while(p != gclist.end());
ReleaseMutex(hMutex);
return memfreed;
}
// Overload assignment of pointer to GCPtr.
template <class T, int size>
T * GCPtr<T, size>::operator=(T *t) {
if(WaitForSingleObject(hMutex, 10000)==WAIT_TIMEOUT)
throw TimeOutExc();
list<GCInfo<T> >::iterator p;
// First, decrement the reference count
// for the memory currently being pointed to.
p = findPtrInfo(addr);
p->refcount--;
// Next, if the new address is already
// existent in the system, increment its
// count. Otherwise, create a new entry
// for gclist.
p = findPtrInfo(t);
if(p != gclist.end())
p->refcount++;
else {
// Create and store this entry.
GCInfo<T> gcObj(t, size);
gclist.push_front(gcObj);
}
addr = t; // store the address.
ReleaseMutex(hMutex);
return t;
}
// Overload assignment of GCPtr to GCPtr.
template <class T, int size>
GCPtr<T, size> & GCPtr<T, size>::operator=(GCPtr &rv) {
if(WaitForSingleObject(hMutex, 10000)==WAIT_TIMEOUT)
throw TimeOutExc();
list<GCInfo<T> >::iterator p;
// First, decrement the reference count
// for the memory currently being pointed to.
p = findPtrInfo(addr);
p->refcount--;
// Next, increment the reference count of
// of the new object.
p = findPtrInfo(rv.addr);
p->refcount++; // increment ref count
addr = rv.addr;// store the address.
ReleaseMutex(hMutex);
return rv;
}
// A utility function that displays gclist.
template <class T, int size>
void GCPtr<T, size>::showlist() {
if(WaitForSingleObject(hMutex, 10000)==WAIT_TIMEOUT)
throw TimeOutExc();
list<GCInfo<T> >::iterator p;
cout << "gclist<" << typeid(T).name() << ", "
<< size << ">:\n";
cout << "memPtr refcount value\n";
if(gclist.begin() == gclist.end()) {
cout << " -- Empty --\n\n";
return;
}
for(p = gclist.begin(); p != gclist.end(); p++) {
cout << "[" << (void *)p->memPtr << "]"
<< " " << p->refcount << " ";
if(p->memPtr) cout << " " << *p->memPtr;
else cout << " ---";
cout << endl;
}
cout << endl;
ReleaseMutex(hMutex);
}
// Find a pointer in gclist.
template <class T, int size>
typename list<GCInfo<T> >::iterator
GCPtr<T, size>::findPtrInfo(T *ptr) {
list<GCInfo<T> >::iterator p;
// Find ptr in gclist.
for(p = gclist.begin(); p != gclist.end(); p++)
if(p->memPtr == ptr)
return p;
return p;
}
// Entry point for garbage collector thread.
template <class T, int size>
unsigned __stdcall GCPtr<T, size>::gc(void * param) {
#ifdef DISPLAY
cout << "Garbage collection started.\n";
#endif
while(isRunning()) {
collect();
}
collect(); // collect garbage on way out
// Release and reset the thread handle so
// that the garbage collection thread can
// be restarted if necessary.
CloseHandle(hThrd);
hThrd = 0;
#ifdef DISPLAY
cout << "Garbage collection terminated for "
<< typeid(T).name() << "\n";
#endif
return 0;
}
// Clear gclist when program exits.
template <class T, int size>
void GCPtr<T, size>::shutdown() {
if(gclistSize() == 0) return; // list is empty
list<GCInfo<T> >::iterator p;
#ifdef DISPLAY
cout << "Before collecting for shutdown() for "
<< typeid(T).name() << "\n";
#endif
for(p = gclist.begin(); p != gclist.end(); p++) {
// Set all remaining reference counts to zero.
p->refcount = 0;
}
collect();
#ifdef DISPLAY
cout << "After collecting for shutdown() for "
<< typeid(T).name() << "\n";
#endif
}
To use the multithreaded garbage collector, include gcthrd.h in your
program. Then, use GCPtr in the same way as described in Chapter 2.
When you compile the program, you must remember to link in the
multithreaded libraries, as explained earlier in this chapter in the
section describing _beginthreadex( ) and endthreadex( ).
To see the effects of the multithreaded garbage collector, try this
version of the load test program originally shown in Chapter 2:
// Demonstrate the multithreaded garbage collector. #include <iostream>
#include <new>
#include "gcthrd.h"
using namespace std;
// A simple class for load testing GCPtr.
class LoadTest {
int a, b;
public:
double n[100000]; // just to take-up memory
double val;
LoadTest() { a = b = 0; }
LoadTest(int x, int y) {
a = x;
b = y;
val = 0.0;
}
friend ostream &operator<(ostream &strm, LoadTest &obj);
};
// Create an insertor for LoadTest.
ostream &operator<(ostream &strm, LoadTest &obj) {
strm << "(" << obj.a << " " << obj.b << ")";
return strm;
}
int main() {
GCPtr<LoadTest> mp;
int i;
for(i = 1; i < 2000; i++) {
try {
mp = new LoadTest(i, i);
if(!(i%100))
cout << "gclist contains " << mp.gclistSize()
<< " entries.\n";
} catch(bad_alloc xa) {
// For most users, this exception won't
// ever occur.
cout << "Last object: " << *mp << endl;
cout << "Length of gclist: "
<< mp.gclistSize() << endl;
}
}
return 0;
}
Here is a sample run. (Of course, your output may vary.) This output
was produced with the display option turned on by defining DISPLAY
within gcthrd.h.
Garbage collection started.
gclist contains 42 entries.
gclist contains 35 entries.
gclist contains 29 entries.
gclist contains 22 entries.
gclist contains 18 entries.
gclist contains 11 entries.
gclist contains 4 entries.
gclist contains 51 entries.
gclist contains 47 entries.
gclist contains 40 entries.
gclist contains 33 entries.
gclist contains 26 entries.
gclist contains 19 entries.
gclist contains 15 entries.
gclist contains 10 entries.
gclist contains 3 entries.
gclist contains 53 entries.
gclist contains 46 entries.
gclist contains 42 entries.
Before collecting for shutdown() for class LoadTest
After collecting for shutdown() for class LoadTest
As you can see, because collect( ) is running in the background,
gclist never gets very large, even though thousands of objects are
being allocated and abandoned.
Some Things to Try
Creating successful multithreaded programs can be quite challenging.
One reason for this is the fact that multithreading requires that you
think of programs in parallel rather than linear terms. Furthermore, at
runtime, threads interact in ways that are often difficult to
anticipate. Thus, you might be surprised (or even bewildered) by the
actions of a multithreaded program. The best way to get good at
multithreading is to play with it. Toward this end, here are some ideas
that you might want to try.
Try adding another list box to the thread control panel that lets
the user adjust the priority class of the thread in addition to its
priority value. Try adding various synchronization objects to the
control panel that can be turned on or off under user control. This
will let you experiment with different synchronization options.
For the multithreaded garbage collector, try collecting garbage less
often, such as when gclist reaches a certain size or after free memory
drops to a predetermined point. Alternatively, you could use a waitable
timer to activate garbage collection on a regular basis. Finally, you
might want to experiment with the garbage collector’s priority class
and settings to find which level is optimal for your use.
Leave your greetings.