Keeping a GUI responsive while running a lengthy process requires either some very elaborate "callbacks" to allow the GUI to process its message queue, or the use of (background) (worker) threads.
Kicking off any number of threads to do some work usually isn't a problem. The fun starts when you want to make the GUI show intermediate and final results or report on the progress.
Showing anything in the GUI requires interacting with controls and/or the message queue/pump. That should always be done in the context of the main thread. Never in the context of any other thread.
There are many ways to handle this.
This example shows how you can do it using simple threads, allowing the GUI to access the thread instance after it is finished by setting FreeOnTerminate
to false
, and reporting when a thread is "done" using PostMessage
.
Notes on race conditions: References to the worker threads are kept in an array in the form. When a thread is finished, the corresponding reference in the array gets nil-ed.
This is a potential source of race conditions. As is the use of a "Running" boolean to make it easier to determine whether there are still any threads that need to finish.
You will need to decide whether you need to protect theses resource using locks or not.
In this example, as it stands, there is no need. They are only modified in two locations: the StartThreads
method and the HandleThreadResults
method. Both methods only ever run in the context of the main thread. As long as you keep it that way and don't start calling these methods from the context of different threads, there is no way for them to produce race conditions.
type
TWorker = class(TThread)
private
FFactor: Double;
FResult: Double;
FReportTo: THandle;
protected
procedure Execute; override;
public
constructor Create(const aFactor: Double; const aReportTo: THandle);
property Factor: Double read FFactor;
property Result: Double read FResult;
end;
The constructor just sets the private members and sets FreeOnTerminate to False. This is essential as it will allow the main thread to query the thread instance for its result.
The execute method does its calculation and then posts a message to the handle it received in its constructor to say its done:
procedure TWorker.Execute;
const
Max = 100000000;var
i : Integer;
begin
inherited;
FResult := FFactor;
for i := 1 to Max do
FResult := Sqrt(FResult);
PostMessage(FReportTo, UM_WORKERDONE, Self.Handle, 0);
end;
The use of PostMessage
is essential in this example. PostMessage
"just" puts a message on the queue of the main thread's message pump and doesn't wait for it to be handled. It is asynchronous in nature. If you were to use SendMessage
you'd be coding yourself into a pickle. SendMessage
puts the message on the queue and waits until it has been processed. In short, it is synchronous.
The declarations for the custom UM_WORKERDONE message are declared as:
const
UM_WORKERDONE = WM_APP + 1;
type
TUMWorkerDone = packed record
Msg: Cardinal;
ThreadHandle: Integer;
unused: Integer;
Result: LRESULT;
end;
The UM_WORKERDONE
const uses WM_APP
as a starting point for its value to ensure that it doesn't interfere with any values used by Windows or the Delphi VCL (as recommended by MicroSoft).
Any form can be used to start threads. All you need to do is add the following members to it:
private
FRunning: Boolean;
FThreads: array of record
Instance: TThread;
Handle: THandle;
end;
procedure StartThreads(const aNumber: Integer);
procedure HandleThreadResult(var Message: TUMWorkerDone); message UM_WORKERDONE;
Oh, and the example code assumes the existence of a Memo1: TMemo;
in the form's declarations, which it uses for "logging and reporting".
The FRunning
can be used to prevent the GUI from starting being clicked while the work is going on. FThreads
is used to hold the instance pointer and the handle of the created threads.
The procedure to start the threads has a pretty straightforward implementation. It starts with a check whether there already is a set of threads being waited on. If so, it just exits. If not, it sets the flag to true and starts the threads providing each with its own handle so they know where to post their "done" message.
procedure TForm1.StartThreads(const aNumber: Integer);
var
i: Integer;
begin
if FRunning then
Exit;
FRunning := True;
Memo1.Lines.Add(Format('Starting %d worker threads', [aNumber]));
SetLength(FThreads, aNumber);
for i := 0 to aNumber - 1 do
begin
FThreads[i].Instance := TWorker.Create(pi * (i+1), Self.Handle);
FThreads[i].Handle := FThreads[i].Instance.Handle;
end;
end;
The thread's handle is also put in the array because that is what we receive in the messages that tell us a thread is done and having it outside the thread's instance makes it slightly easier to access. Having the handle available outside the thread's instance also allows us to use FreeOnTerminate
set to True
if we didn't need the instance to get its results (for example if they had been stored in a database). In that case there would of course be no need to keep a reference to the instance.
The fun is in the HandleThreadResult implementation:
procedure TForm1.HandleThreadResult(var Message: TUMWorkerDone);
var
i: Integer;
ThreadIdx: Integer;
Thread: TWorker;
Done: Boolean;
begin
// Find thread in array
ThreadIdx := -1;
for i := Low(FThreads) to High(FThreads) do
if FThreads[i].Handle = Cardinal(Message.ThreadHandle) then
begin
ThreadIdx := i;
Break;
end;
// Report results and free the thread, nilling its pointer and handle
// so we can detect when all threads are done.
if ThreadIdx > -1 then
begin
Thread := TWorker(FThreads[i].Instance);
Memo1.Lines.Add(Format('Thread %d returned %f', [ThreadIdx, Thread.Result]));
FreeAndNil(FThreads[i].Instance);
FThreads[i].Handle := nil;
end;
// See whether all threads have finished.
Done := True;
for i := Low(FThreads) to High(FThreads) do
if Assigned(FThreads[i].Instance) then
begin
Done := False;
Break;
end;
if Done then
begin
Memo1.Lines.Add('Work done');
FRunning := False;
end;
end;
This method first looks up the thread using the handle received in the message. If a match was found, it retrieves and reports the thread's result using the instance (FreeOnTerminate
was False
, remember?), and then finishes up: freeing the instance and setting both the instance reference and the handle to nil, indicating this thread is no longer relevant.
Finally it checks to see if any of the threads is still running. If none is found, "all done" is reported and the FRunning
flag set to False
so a new batch of work can be started.