2004. július 29., csütörtök

Waiting for Threads


Problem/Question/Abstract:

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:

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, @thrArr, 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, @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.

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...

Nincsenek megjegyzések:

Megjegyzés küldése