Author: Lou Adler
I've created an application that spawns a couple of threads at once when a user
presses a button. The threads execute fairly quickly, but I don't want the user to
move on to the next task until the threads are absolutely finished. I could move
the thread code back into the main unit, but that defeats the whole purpose of
unlocking my user interface while the processes take place. Is there a good way to
do this?
Answer:
In past articles regarding threads, I've discussed Critical Sections and mutexes as
ways of protecting shared resources, and having them wait until a resource is
freed. But how do you wait for a thread or threads to finish from the user
interface?
There are a couple of ways to approach this. First, you create a global Boolean
variable and use it as a flag. You set its value when the thread starts, then set
its value when the thread ends. Meanwhile, you have can set up a looping mechanism
in the main unit to periodically check the completion status of the thread and use
the Application.ProcessMessages call to keep your application alive. Here's some
code:
1 unit wthread;
2
3 interface
4
5 uses
6 Classes, Windows;
7
8 type
9 TTestThr = class(TThread)
10 private
11 { Private declarations }
12 protected
13 procedure Execute; override;
14 end;
15
16 implementation
17 uses Main;
18 { TTestThr }
19
20 procedure TTestThr.Execute;
21 var
22 I: Integer;
23 begin
24 for I := 0 to 39 do
25 Sleep(500);
26 ImDone := True;
27 end;
28
29 end.
The code above is the thread code. All it does is perform a for loop and wait for
half a second in between increments. Pretty basic stuff. Here's the OnClick method
for a button on the main form that starts the thread:
30 procedure TForm1.Button1Click(Sender: TObject);
31 begin
32 ImDone := False;
33 Label1.Caption := 'Waiting';
34 TTestThr.Create(False);
35 while not ImDone do
36 Application.ProcessMessages;
37 Label1.Caption := 'Not Waiting';
38 end;
The var ImDone is a Boolean flag that gets set as soon as the button is pressed. As
you can see, the while loop in the OnClick handler just performs a yield with
Application.ProcessMessages to process user input. Pretty simple right? Okay, now,
let me tell you one very important thing:
Disregard everything that I just wrote! It's the wrong way to do it!
Why? For one thing, it's totally inefficient. While Application.ProcessMessages
allows a program to continue to receive and process messages it receives, it still
eats up CPU time because all it's doing is yielding temporarily, then going back to
its original state, meaning it constantly activates and deactives. Yikes! Not good.
With a thread like the TTestThr that I just wrote, you won't notice much of a
difference in performance by using this methodology. But that doesn't mean that
it's right. Furthermore, if you're a real stickler for good structured programming
methodologies, you should avoid global variables like the plague! But that's not
even the worst of it. There's a real big catch here...
What do you do in the case of having to wait for multiple threads to finish? In the
model described above, you would have to create a Boolean flag for EVERY thread
that you create! Now that's bad.
The best way to handle this condition is to create what might be called a wait
thread - an intermediary thread that will perform the waiting. In combination with
a couple of Windows API calls, you can achieve a very efficient thread waiting
process at little cost to CPU time and system resources.
Those couple of Windows API functions are called WaitForSingleObject and
WaitForMultipleObjects. These two functions are used to wait for objects to enter a
signaled state before they return. Okay, what's signaled mean anyway? This one took
me awhile to understand, but for you, I'll be as clear as possible so you can
understand it much quicker than I did. Essentially, when an object is created in
Windows, it is given a system assigned state property of sorts. While it is active
or in use, it is said to be non-signaled. When it is available, it is said to be
signaled. I know, it seems kind of backwards. But that's it in a nutshell. Anyway,
with respect to the functions above, they are designed to wait for an object or
objects to enter a signaled state; that is, wait for the objects to become
available to the system again.
The advantage of these two functions with respect to waiting for threads to finish
is that they enter into an efficient sleep state that consumes very little CPU
time. Contrast that with the Application.ProcessMessages methodology which has the
potential for consuming CPU cycles, and you know why they'd be the choice make if
you're going to wait for threads.
WaitForSingleObject takes two parameters, a THandle and a timeout value in
milliseconds. If you're going to wait for a single thread, all you need to do is
pass the thread's handle and the time amount of time to wait for the object and
it'll do the waiting for you. I should mention that there's a special system
constant called INFINITE that will make the function wait indefinitely. Typically,
you'll use this constant as opposed to setting a specific time. But that also
depends on your process. Here's an example:
WaitForSingleObject(MyThread.Handle, INFINITE);
On the other hand, WaitForMultipleObjects is a bit more complex. It takes for
arguments for parameters. They're described in the table below (don't worry about
them too much right now, we'll discuss them below):
Argument Type Description
cObject DWORD Number of handles in the object handle array
lphObjects Pointer Address of object handle array
fWaitAll Boolean True indicates that the function waits until all objects are
signaled
dwTimeOut DWORD Number of milliseconds to wait (can be INFINITE)
What's this about a handle array? Well, in order for the function to track the
states of all objects to be waited for, their handles have to be in an array. This
is simple to create: just declare an array of THandle and set each element's value
to a thread's handle. No big deal. But I think it's probably best to put all this
perspective with some code that we can discuss. Here's the entire unit code that
contains both the wait thread's declaration and the TTestThr declaration:
39 unit wthread;
40
41 interface
42
43 uses
44 Classes, Windows;
45
46 //"Worker thread declaration"
47 type
48 TTestThr = class(TThread)
49 private
50 { Private declarations }
51 protected
52 procedure Execute; override;
53 end;
54
55 //Wait thread declaration
56 type
57 TWaitThr = class(TThread)
58 private
59 procedure UpdateLabel; //this just sets the label's caption
60 protected
61 procedure Execute; override;
62 end;
63
64 implementation
65 uses Main;
66 { TTestThr }
67
68 procedure TTestThr.Execute;
69 var
70 I: Integer;
71 begin
72 FreeOnTerminate := True;
73 for I := 0 to 39 do
74 Sleep(500);
75 ImDone := True;
76 end;
77
78 procedure TWaitThr.UpdateLabel;
79 begin
80 Form1.Label1.Caption := 'Not Waiting';
81 end;
82
83 procedure TWaitThr.Execute;
84 var
85 hndlArr: array[0..4] of THandle;
86 thrArr: array[0..4] of TTestThr;
87 I: Integer;
88 begin
89 FreeOnTerminate := True;
90 for I := 0 to 4 do
91 begin
92 thrArr[I] := TTestThr.Create(False);
93 hndlArr[I] := thrArr[I].Handle;
94 Sleep(1000); //stagger creation of the threads
95 end;
96 WaitForMultipleObjects(5, @thrArr, True, INFINITE);
97 Synchronize(UpdateLabel);
98 end;
99
100 end.
I put the Execute method for the TWaitThr in boldface so you can focus in on it.
Notice that to fill in the hndlArr elements, I use a simple for loop. To make
matters even simpler, I just declared an array of TTestThr to I could create the
threads and immediately assign their respective handles to the handle array. The
most important line in the code is:
WaitForMultipleObjects(5, @thrArr, True, INFINITE);
which is the call to WaitForMultipleObjects. Notice too that I pass the handle
array's address to the function, as that is what the function calls for. Once the
call is made, it won't allow the Execute method to continue until all the threads
enter a signaled state. Once it's done waiting, UpdateLabel is called to change the
text of a label on my main form. Here's the entire code listing for the main form.
All it has on it are a TLabel and two TButtons.
101 unit main;
102
103 interface
104
105 uses
106 Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
107 StdCtrls;
108
109 type
110 TForm1 = class(TForm)
111 Label1: TLabel;
112 Button1: TButton;
113 Button2: TButton;
114 procedure Button1Click(Sender: TObject);
115 procedure Button2Click(Sender: TObject);
116 private
117 { Private declarations }
118 public
119 { Public declarations }
120 end;
121
122 var
123 Form1: TForm1;
124 ImDone: Boolean;
125
126 implementation
127 uses wthread;
128 {$R *.DFM}
129
130 procedure TForm1.Button1Click(Sender: TObject);
131 begin
132 ImDone := False;
133 Label1.Caption := 'Waiting';
134 TTestThr.Create(False);
135 while not ImDone do
136 Application.ProcessMessages;
137 Label1.Caption := 'Not Waiting';
138 end;
139
140 procedure TForm1.Button2Click(Sender: TObject);
141 begin
142 Label1.Caption := 'Waiting';
143 TWaitThr.Create(False);
144 end;
145
146 end.
So why go to all this trouble? Why not move the WaitForMultipleObjects code to the
main form? The reason for this is simple. Since both WaitForSingleObject and
WaitForMultipleObjects don't return until the object(s) they're waiting for enter a
signaled state, the main form would essentially become locked and unavailable until
the function returns. Kind of defeats the whole purpose of writing multi- threaded
programs don't you think?
So here's another thing that you can add to your arsenal of multi-threading techniques...
|