수안이의 컴퓨터 연구실

  • Mainpage
  • About Me
  • Tags
  • Metapage
  • Notice
  • Location
  • Keywords
  • Guestbook
  • Admin
  • Write an Article
  • Total | 1693868
  • Today | 219
  • Yesterday | 588

Programming/C++2007/07/30 10:29

Multithreading in C++

출처 : http://www.devarticles.com/c/a/cplusplu ··· -in-c%2F

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:

  1. Include tcp.cpp in your program.
  2. Include tcp.rc in your program’s resource file.
  3. Create the thread or threads that you want to control.
  4. 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:

  1. 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.
  2. 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.
  3. Another exception must be defined that will be used to indicate a time-out condition.
  4. The GCPtr destructor must no longer call collect( ). Garbage collection is handled by the garbage collection thread.
  5. A function called gc( ) that serves as the thread entry point for the garbage collector must be defined.
  6. A function called isRunning( ) must be defined. It returns true if the garbage collection is in use.
  7. 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.


"C++" 카테고리의 다른 글
  • C++ Preprocessor: Always Assert Your Code Is Right (0)2007/07/30
  • C++ Preprocessor: The Code in the Middle (0)2007/07/30
  • Multithreading in C++ (0)2007/07/30
  • A Simple Garbage Collector for C++ (0)2007/07/30
  • Function Pointers (1)2007/07/30
2007/07/30 10:29 2007/07/30 10:29
Posted by webdizen
Tags C++Keyword C++, Multithreading
No Trackback No Comment

Trackback URL : http://www.webdizen.net/blog/trackback/3108

Leave your greetings.

[로그인][오픈아이디란?]

«Prev  1 ... 154 155 156 157 158 159 160 161 162 ... 3009  Next»

RSS HanRSS
Blog Image
webdizen
이곳은 컴퓨터에 대해 연구하고, 공유하고, 소통하기 위한 연구실입니다. 개인적으로는 OLAP, Data Mining, Semantic Web, Data Modeling에 대해서 연구하고 있습니다.

Categories

전체 (3009)
Webdizen (141)
Life (6)
Diary (16)
Blog (9)
IDEA (2)
Travel (10)
Book (16)
Photo (7)
Movie (8)
Music (14)
Leisure Sports (10)
Funny (6)
Hardware (121)
Software (120)
Windows (5)
Unix & Linux (120)
Installation (5)
Kernel (10)
System (34)
Develop (22)
X-Window (0)
Applicaton (31)
Security (4)
Framework (2)
Hadoop (2)
Programming (804)
Algorithm & Data Structure (1)
Assembly (38)
UNIX/Linux C (95)
C++ (128)
STL (4)
Java (38)
Win32 API (92)
ATL/COM (44)
MFC (151)
.NET (26)
WCF/WPF (4)
C# (28)
Network Programming (17)
Database Programming (12)
OpenGL / DirectX (13)
Multimedia Programming (0)
Game Programming (21)
Parallel Distributed Progra... (0)
Reverse Engineering (0)
Debugging (9)
Python (1)
Ruby (1)
Ruby on Rails (1)
QT (4)
GTK (0)
JSP (0)
PHP (6)
ASP.NET (6)
ASP (2)
Development (28)
Useful Library (2)
Data Modeling (0)
Database (105)
Oracle (4)
MSSQL (41)
MySQL (2)
Data Warehouse (2)
Data Mining (4)
Network (66)
Web (79)
DHTML (4)
XHTML (1)
Javascript (1)
CSS (1)
AJAX (9)
XML (11)
Flex (1)
Silverlight (3)
Security (91)
DoS (1)
Kernel (10)
Scanning (3)
Sniffing (0)
Spoofing (4)
Overflow (28)
Web (11)
Shell (10)
Format String (14)
Window (2)
Embedded (70)
Multimedia (27)
Mobile (14)
Graphic (24)
Management (633)
Knowledge (581)
Hadoop (0)

Notice

  • 메타 블로그 사이트에 등록
  • 새해 맞이 블로그의 변화
  • 블로그 명칭 변경
  • 도메인(www.webdizen.net) 구...
  • TEXTCUBE 1.6.1로 업그레이드...

Tags

  • Web Tool
  • egrep
  • Community
  • Baseline
  • Chat
  • Web Services
  • /etc/shadow
  • 화면 보호기 제어
  • Standards
  • 커널 디버깅
  • SARG
  • 저장소 엔진
  • 감정이입
  • 블루투스
  • XP
  • 공유 스캔
  • Event
  • 캡슐카
  • Windows Fresentation Foundation
  • BOM

Recent Articles

  • 트위터(Twitter)의 시작!.
  • 청년 리더의 조건.
  • 애플의 타블렛 PC - 아이패드....
  • 미래의 인터페이스 - 육감 기....
  • 기초발성법 동영상 강좌.

Recent Comments

  • 학교 과제물중 쓰레드에 대하....
    장진혁 03/17
  • 관리자만 볼 수 있는 댓글입....
    비밀방문자 03/12
  • 상대방의 이야기를 열심히 경....
    DoNuts 03/03
  • Lots of students know techn....
    Bobbi35Shannon 02/25
  • 좋은글 잘 보고 갑니다..
    Und_hacker 01/08

Recent Trackbacks

  • printf,scanf를 이용한 형식....
    yundream의 프로그래밍 이야기 03/10
  • 파일 열기/저장하기 CFileDialog.
    은마군의 나태블록 2009
  • World IT Show 2008.
    상우 :: Oranzie's BLOG 2008
  • cvs서버 설치하기.
    3인3색 2008
  • 속속 공개되는 Google Chart....
    PHP와 Web 2.0 2007

Archive

  • 2010/02 (1)
  • 2010/01 (6)
  • 2009/12 (5)
  • 2009/09 (3)
  • 2009/08 (1)

Calendar

«   2010/03   »
일 월 화 수 목 금 토
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31      

Bookmarks

    • Administration
      • IIS.NET
      • NTFAQ
      • OS의 모든 것
      • 리눅스포털
    • Database
      • SQL Server Central
      • SQL Team
    • Development
      • .NET Heaven
      • ASP Alliance
      • ASP.NET 2.0
      • Bullog.net
      • C# Corner
      • C++ (C PlusPlus.com)
      • C++ Reference
      • CodeGuru
      • CodePlex
      • DebugLab
      • Dev Articles
      • Devpia
      • DotNet Junkies
      • DotNet Zone
      • Driver Online
      • GOSU.NET
      • HOONS 닷넷
      • Joinc 팀블로그
      • KOSR
      • MSDN Home Page
      • OSR Online
      • Sky.ph - 개발자 커뮤니...
      • TAEYO.NET
      • The Code Project
      • WindowsClient.net
      • 김상욱의 개발자 Side
      • 조인시 위키
    • Human Networks
      • belief21c's e-space
      • I think I can
      • Invisible Rover's Blog :D
      • Rodman®
      • ■ Feel So Good~! ■
      • 까만 나비
      • 나를 가꾸는 시간.
      • 나만의 즐거움~~!
      • 단녕
      • 상우 :: Oranzie's BLOG
    • Information Technology
      • Microsoft TechNet
      • 지디넷코리아 - 글로벌...
    • Security
      • FoundStone
      • milw0rm
      • NewOrder
      • OpenRCE
      • Phrack.org
      • Reverse Engineering b1...
      • Reverse Engineering Team
      • RootKit
      • SecurityFocus
      • SecurityXploded by Nag...
      • Wow Hacker
      • Zone-H
Textcube
Louice Studio Inc.
Powered by Textcube. Original designed by Tistory.