Multithreading in C++ - A Closer Look at the Thread Control Panel
(Page 6 of 11 )
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( ).
Next: Demonstrating the Control Panel >>
More C++ Articles
More By McGraw-Hill/Osborne
|
This article is taken from chapter three of The Art of C++, written by Herbert Schildt (McGraw-Hill/Osborne, 2004; ISBN: 0072255129). Check it out at your favorite bookstore. Buy this book now.
|
|