23

I've always wondered is there a better way that I should be writing some of my procedures, particularly ones that take a long time to finish.

I have always run everything off the Main GUI Thread which I now understand and realise is bad because it will make the Application unresponsive, Application.ProcessMessages will not really help here.

This makes me think I need to use TThreads for lengthy operations such as copying a file for example. This is also made me wonder how some Applications give you full control, eg allow you to pause, resume and or stop the operation.

I have about 3 lengthy operations in a personal project I am working on which I display a dialog form with a TProgressBar on. Whilst this does work, I feel it could be done much better. These progress dialogs could be shown for such a long time that you may want to cancel the operation and instead finish the job later.

As I said, currently I am running of the Main Gui Thread, do I instead need to use TThreads? I am not sure how or where to start implementing them as I have not worked with them before. If I do need threads do they offer what I need such as pausing, resuming, stopping an operation etc?

Basically I am looking for a better way of handling and managing lengthy operations.

  • 6
    You need to signal to the thread that you want to pause or cancel. And the thread must check for that signal. – David Heffernan Jun 30 '12 at 21:54
  • or you can suspend and then resume it. A signal like a global var is better and more organized. You could also use Mutex as a signal... – Benjamin Weiss Jun 30 '12 at 22:04
  • 4
    @Benjamin Suspend and Resume? Not really. Those Windows functions should not be used. – David Heffernan Jun 30 '12 at 22:09
  • That's what there are for... to use them... – Benjamin Weiss Jun 30 '12 at 22:13
  • 7
    @BenjaminWeiss Not so. [`TThread.Suspend`](http://docwiki.embarcadero.com/Libraries/en/System.Classes.TThread.Suspend): *Pauses a running thread. Suspend was intended to be used by debuggers and is deprecated in RAD Studio XE, in the year 2010.* The only way to do this reliably is to get the thread to check regularly for a pause or cancel signal. – David Heffernan Jun 30 '12 at 22:14
  • I'm talking about the WindowsAPI. Not about the TThread Class. – Benjamin Weiss Jun 30 '12 at 22:16
  • 5
    @Benjamin Then read the documentation for SuspendThread. It says the same. Think about it. How do you suppose TThread.Suspend is implemented? Go on, take a wild guess at which Windows API is called. – David Heffernan Jun 30 '12 at 22:19

4 Answers4

17

Yes, this is definitely a case where you need a thread to do the task.

A little example how to pause/resume a thread and cancel the thread.

Progress is sent to the main thread through a PostMessage call. The pause/resume and cancel are made with TSimpleEvent signals.

Edit: As per the comments from @mghie, here is a more complete example:

Edit 2: Showing how to pass a procedure for the thread to call for the heavy work.

Edit 3: Added some more features and a test unit.

unit WorkerThread;

interface

uses Windows, Classes, SyncObjs;

type
  TWorkFunction = function: boolean of object;

  TWorkerThread = Class(TThread)
  private
    FCancelFlag: TSimpleEvent;
    FDoWorkFlag: TSimpleEvent;
    FOwnerFormHandle: HWND;
    FWorkFunc: TWorkFunction; // Function method to call
    FCallbackMsg: integer; // PostMessage id
    FProgress: integer;
    procedure SetPaused(doPause: boolean);
    function GetPaused: boolean;
    procedure Execute; override;
  public
    Constructor Create(WindowHandle: HWND; callbackMsg: integer;
      myWorkFunc: TWorkFunction);
    Destructor Destroy; override;
    function StartNewWork(newWorkFunc: TWorkFunction): boolean;
    property Paused: boolean read GetPaused write SetPaused;
  end;

implementation

constructor TWorkerThread.Create(WindowHandle: HWND; callbackMsg: integer;
  myWorkFunc: TWorkFunction);
begin
  inherited Create(false);
  FOwnerFormHandle := WindowHandle;
  FDoWorkFlag := TSimpleEvent.Create;
  FCancelFlag := TSimpleEvent.Create;
  FWorkFunc := myWorkFunc;
  FCallbackMsg := callbackMsg;
  Self.FreeOnTerminate := false; // Main thread controls for thread destruction
  if Assigned(FWorkFunc) then
    FDoWorkFlag.SetEvent; // Activate work at start
end;

destructor TWorkerThread.Destroy; // Call MyWorkerThread.Free to cancel the thread
begin
  FDoWorkFlag.ResetEvent; // Stop ongoing work
  FCancelFlag.SetEvent; // Set cancel flag
  Waitfor; // Synchronize
  FCancelFlag.Free;
  FDoWorkFlag.Free;
  inherited;
end;

procedure TWorkerThread.SetPaused(doPause: boolean);
begin
  if doPause then
    FDoWorkFlag.ResetEvent
  else
    FDoWorkFlag.SetEvent;
end;

function TWorkerThread.StartNewWork(newWorkFunc: TWorkFunction): boolean;
begin
  Result := Self.Paused; // Must be paused !
  if Result then
  begin
    FWorkFunc := newWorkFunc;
    FProgress := 0; // Reset progress counter
    if Assigned(FWorkFunc) then
      FDoWorkFlag.SetEvent; // Start work
  end;
end;

procedure TWorkerThread.Execute;
{- PostMessage LParam:
  0 : Work in progress, progress counter in WParam
  1 : Work is ready
  2 : Thread is closing
}
var
  readyFlag: boolean;
  waitList: array [0 .. 1] of THandle;
begin
  FProgress := 0;
  waitList[0] := FDoWorkFlag.Handle;
  waitList[1] := FCancelFlag.Handle;
  while not Terminated do
  begin
    if (WaitForMultipleObjects(2, @waitList[0], false, INFINITE) <>
      WAIT_OBJECT_0) then
      break; // Terminate thread when FCancelFlag is signaled
    // Do some work
    readyFlag := FWorkFunc;
    if readyFlag then // work is done, pause thread
      Self.Paused := true;
    Inc(FProgress);
    // Inform main thread about progress
    PostMessage(FOwnerFormHandle, FCallbackMsg, WPARAM(FProgress),
      LPARAM(readyFlag));
  end;
  PostMessage(FOwnerFormHandle, FCallbackMsg, 0, LPARAM(2)); // Closing thread
end;

function TWorkerThread.GetPaused: boolean;
begin
  Result := (FDoWorkFlag.Waitfor(0) <> wrSignaled);
end;

end.

Just call MyThread.Paused := true to pause and MyThread.Paused := false to resume the thread operation.

To cancel the thread, call MyThread.Free.

To receive the posted messages from the thread, see following example:

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
  System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, WorkerThread;

const
  WM_MyProgress = WM_USER + 0; // The unique message id

type
  TForm1 = class(TForm)
    Label1: TLabel;
    btnStartTask: TButton;
    btnPauseResume: TButton;
    btnCancelTask: TButton;
    Label2: TLabel;
    procedure btnStartTaskClick(Sender: TObject);
    procedure btnPauseResumeClick(Sender: TObject);
    procedure btnCancelTaskClick(Sender: TObject);
  private
    { Private declarations }
    MyThread: TWorkerThread;
    workLoopIx: integer;

    function HeavyWork: boolean;
    procedure OnMyProgressMsg(var Msg: TMessage); message WM_MyProgress;
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

{ TForm1 }
const
  cWorkLoopMax = 500;

function TForm1.HeavyWork: boolean; // True when ready
var
  i, j: integer;
begin
  j := 0;
  for i := 0 to 10000000 do
    Inc(j);
  Inc(workLoopIx);
  Result := (workLoopIx >= cWorkLoopMax);
end;

procedure TForm1.btnStartTaskClick(Sender: TObject);
begin
  if not Assigned(MyThread) then
  begin
    workLoopIx := 0;
    btnStartTask.Enabled := false;
    btnPauseResume.Enabled := true;
    btnCancelTask.Enabled := true;
    MyThread := TWorkerThread.Create(Self.Handle, WM_MyProgress, HeavyWork);
  end;
end;

procedure TForm1.btnPauseResumeClick(Sender: TObject);
begin
  if Assigned(MyThread) then
    MyThread.Paused := not MyThread.Paused;
end;

procedure TForm1.btnCancelTaskClick(Sender: TObject);
begin
  if Assigned(MyThread) then
  begin
    FreeAndNil(MyThread);
    btnStartTask.Enabled := true;
    btnPauseResume.Enabled := false;
    btnCancelTask.Enabled := false;
  end;
end;

procedure TForm1.OnMyProgressMsg(var Msg: TMessage);
begin
  Msg.Msg := 1;
  case Msg.LParam of
    0:
      Label1.Caption := Format('%5.1f %%', [100.0 * Msg.WParam / cWorkLoopMax]);
    1:
      begin
        Label1.Caption := 'Task done';
        btnCancelTaskClick(Self);
      end;
    2:
      Label1.Caption := 'Task terminated';
  end;
end;

end.

And the form:

object Form1: TForm1
  Left = 0
  Top = 0
  Caption = 'Form1'
  ClientHeight = 163
  ClientWidth = 328
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -13
  Font.Name = 'Tahoma'
  Font.Style = []
  OldCreateOrder = False
  PixelsPerInch = 120
  TextHeight = 16
  object Label1: TLabel
    Left = 79
    Top = 18
    Width = 51
    Height = 16
    Caption = 'Task idle'
  end
  object Label2: TLabel
    Left = 32
    Top = 18
    Width = 41
    Height = 16
    Caption = 'Status:'
  end
  object btnStartTask: TButton
    Left = 32
    Top = 40
    Width = 137
    Height = 25
    Caption = 'Start'
    TabOrder = 0
    OnClick = btnStartTaskClick
  end
  object btnPauseResume: TButton
    Left = 32
    Top = 71
    Width = 137
    Height = 25
    Caption = 'Pause/Resume'
    Enabled = False
    TabOrder = 1
    OnClick = btnPauseResumeClick
  end
  object btnCancelTask: TButton
    Left = 32
    Top = 102
    Width = 137
    Height = 25
    Caption = 'Cancel'
    Enabled = False
    TabOrder = 2
    OnClick = btnCancelTaskClick
  end
end
LU RD
  • 32,988
  • 5
  • 71
  • 260
  • 1
    This is actually not that great as example code. Your paused threads will still wake up several dozen times a second. And how will you synchronize thread shutdown and destruction of externally owned event objects? Simply signal them, wait a bit and hope for the best? You either need to use reference counting for shared objects or free the threads before the events are freed (i.e. not use `FreeOnTerminate`) to do this the proper way. – mghie Jul 01 '12 at 03:49
  • @mghie, you are correct, I made the example more complete. The thread now is more inactive in the paused state and the main thread controls the life time of the thread. – LU RD Jul 01 '12 at 09:08
  • I edited your code to not retrieve the form handle from the thread `Execute()` method but from the constructor. Thus if the form handle is invalidated then `PostMessage()` will fail instead of recreating the `HWND` from the wrong thread context. – mghie Jul 01 '12 at 10:21
  • I can recall a `Suspend` method on TThread in delphi. Has that finally been removed for portability (and stability!) or are you missing it? – Jonas Schäfer Jul 01 '12 at 11:16
  • 2
    @JonasWielicki, `Suspend` and `Resume` was never indented to be used by any other processes than a debugger. They are deprecated since Delphi-XE. See [documentation](http://docwiki.embarcadero.com/Libraries/en/System.Classes.TThread.Suspend). – LU RD Jul 01 '12 at 11:26
  • @LURD okay, I've been out of delphi for some time and thus was just curious. Thanks for the info. – Jonas Schäfer Jul 01 '12 at 11:29
  • @mghie, thanks for the edit. Usually I have a global allocated handle (using Windows.RegisterClass and CreateWindow in the main thread) from where I distribute messages. This will avoid the problems with handles getting recreated. The way to do it is described by [Martin James](http://stackoverflow.com/users/758133/martin-james) [here](http://borland.newsgroups.archived.at/public.delphi.language.delphi.win32/200603/060330160.html). – LU RD Jul 01 '12 at 11:51
  • 1
    @LU RD: Indeed, I should probably have modified the constructor parameter to just be a `HWND` instead of a form reference; it would still have been possible to pass the form handle, but also to pass the handle of a helper window. – mghie Jul 01 '12 at 11:57
  • looks rather complex as I had feared. If I needed to do this for other lengthy operations do I need to rewrite the whole code for each different operation, or can I use the above as a template for each operation so to speak? –  Jul 01 '12 at 12:58
  • @Blobby, you can do the heavy work in a procedure/function call from the thread execute method. You can modify the thread to accept a reference to procedure/function or method during creation. This will let you reuse as much code as possible. – LU RD Jul 01 '12 at 13:04
  • 1
    @Blobby, added an example how to pass a procedure for the heavy work. – LU RD Jul 01 '12 at 13:48
  • @LURD thanks for updating with an example showing how to do this. I like the way you have done this answer, `MyThread := TMyThread.Create( MyForm,HeavyWork);` is a really nice way of creating a new thread. If I understand correctly then, I could just add another procedure as in your example eg HeavyWork2 and I could create it like this: `MyThread := TMyThread.Create( MyForm,HeavyWork2);` –  Jul 01 '12 at 20:30
  • 1
    @Blobby, that's correct. You can create as many work procedures as needed, provided they have the same calling structure. For declaring procedural types of various formats, see [DocWiki Procedural_Types](http://docwiki.embarcadero.com/RADStudio/en/Procedural_Types). – LU RD Jul 01 '12 at 20:51
  • @LURD Good stuff and thanks for the link. Let me study your answer some more and test the example out before I accept should I have any more questions to ask. –  Jul 01 '12 at 21:03
  • 1
    Glad it helped. I will upload a more complete example with some more features. – LU RD Jul 03 '12 at 13:10
  • +1 for updating, this added example is even more useful because I can see I was still not implementing it the best possible way. And I am sure this updated answer will prove useful to others who are looking for a way to implement threading with pause and resume just like I was. And thank you for putting some comments in, this makes it that little easier to see what is going on. –  Jul 03 '12 at 17:20
  • +1 this is a nice example! I am going to use your example and rewrite the class a little bit like passing the class through WPARAM (I don't need the counter really). A question would be... How does the Thread free itself through the mainthread? I don't get it... Could you please explain this? Thank you in advance. – Benjamin Weiss Nov 27 '16 at 15:58
  • 1
    @BenjaminWeiss, calling `FreeAndNil(MyThread)` will call the overridden `Destroy` destructor, that will signal the FCancelFlag, which in turns wakes the thread and breaks its loop and the thread self-destructs while posting a message informing the receiver that it is about to be terminated. Note that you need to study mgie's and my comments about the forms handle. It can be recreated at any time, and the safest way to post messages is to post them to a handle that is not tied to a control. `Application.Handle` is one example. From there, the message can be redirected. – LU RD Nov 27 '16 at 22:20
  • 1
    @BenjaminWeiss, the reason why it is safe is because the message is handled in the main thread and it is safe to call a controls handle from the main thread. Also read my link in a comment above to an example written by Martin James how to do a similar redirection. – LU RD Nov 27 '16 at 22:25
  • The example you have shown has a bug: _if you let the task finish it will show **task terminated** on the label and not **task done**_ because you are calling `btnCancelTaskClick(Self);` after the label's caption is set to _task done_ which will destroy the thread which will wake up the cancel event and breaks the loop resulting in posting the second message which will change the caption to _task terminated_. the solution would be to add this line `if not readyFlag then` before the call of the second `postmessage`. I have suggested an edit I hope you do not mind – Nasreddine Galfout Nov 06 '17 at 23:20
6

You can also use higher level libraries for threading, like:

Harriv
  • 5,877
  • 6
  • 40
  • 73
3

If the sample code in the answer by LU RD is too complicated for your taste then maybe a Delphi implementation of the .net BackgroundWorker class is more to your liking.

Using this you can drop a component onto your form and add handlers for its various events (OnWork, OnWorkProgress, OnWorkFeedback and OnWorkComplete). The component will execute the OnWork event handler in the background, while executing the other event handlers from the GUI thread (taking care of the necessary context switches and synchronization). However, a thorough understanding of what you can and what you must not do from secondary threads is still necessary for writing code in the OnWork event handler.

Community
  • 1
  • 1
mghie
  • 31,279
  • 6
  • 82
  • 123
0

A useful introduction to multithreading was written by a guy called Martin Harvey, many years ago. His tutorial can be found at the Embarcadero CC site - it also looks like he has uploaded an example class which does the kind of thing you are looking for, but I haven't looked at it so cannot say for sure.

Drew Gibson
  • 1,559
  • 3
  • 17
  • 22