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?
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:
unit wthread;
interface
uses
Classes, Windows;
type
TTestThr = class(TThread)
private
{ Private declarations }
protected
procedure Execute; override;
end;
implementation
uses Main;
{ TTestThr }
procedure TTestThr.Execute;
var
I : Integer;
begin
for I := 0 to 39 do
Sleep(500);
ImDone := True;
end;
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:
procedure TForm1.Button1Click(Sender: TObject);
begin
ImDone := False;
Label1.Caption := 'Waiting';
TTestThr.Create(False);
while NOT ImDone do
Application.ProcessMessages;
Label1.Caption := 'Not Waiting';
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:
unit wthread;
interface
uses
Classes, Windows;
//"Worker thread declaration"
type
TTestThr = class(TThread)
private
{ Private declarations }
protected
procedure Execute; override;
end;
//Wait thread declaration
type
TWaitThr = class(TThread)
private
procedure UpdateLabel;//this just sets the label's caption
protected
procedure Execute; override;
end;
implementation
uses Main;
{ TTestThr }
procedure TTestThr.Execute;
var
I : Integer;
begin
FreeOnTerminate := True;
for I := 0 to 39 do
Sleep(500);
ImDone := True;
end;
procedure TWaitThr.UpdateLabel;
begin
Form1.Label1.Caption := 'Not Waiting';
end;
procedure TWaitThr.Execute;
var
hndlArr : Array[0..4] of THandle;
thrArr : Array[0..4] of TTestThr;
I : Integer;
begin
FreeOnTerminate := True;
for I := 0 to 4 do begin
thrArr[I] := TTestThr.Create(False);
hndlArr[I] := thrArr[I].Handle;
Sleep(1000); //stagger creation of the threads
end;
WaitForMultipleObjects(5, @hndlArr, True, INFINITE);
Synchronize(UpdateLabel);
end;
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, @hndlArr, 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.
unit main;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls;
type
TForm1 = class(TForm)
Label1: TLabel;
Button1: TButton;
Button2: TButton;
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
ImDone : Boolean;
implementation
uses wthread;
{$R *.DFM}
procedure TForm1.Button1Click(Sender: TObject);
begin
ImDone := False;
Label1.Caption := 'Waiting';
TTestThr.Create(False);
while NOT ImDone do
Application.ProcessMessages;
Label1.Caption := 'Not Waiting';
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
Label1.Caption := 'Waiting';
TWaitThr.Create(False);
end;
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...
Copyright © 1997 Brendan V. Delumpa All Rights Reserved
|