Working with threads and forms

This forum is a new user friendly zone. If you're new and have questions, this is the place to get started.
tudi_x
Posts: 2
Joined: Sun Apr 05, 2015 11:33 am

Working with threads and forms

Postby tudi_x » Mon Apr 06, 2015 6:41 pm

Hi,
Some time ago I started to write an SQL database client. A basic application where you send a query to a database and after some time you receive the information and place it in a DBGrid. Due to the asynchronous way of receiving the query results this kind of application would require a thread so the interface main thread does not block the interface.
My app would have a main form with a subform pool (TObjectList) - from this main form I would open SQL execution windows and from each SQL execution window I could connect to a different database and also open a database object browser window (actually a table browser).

The thread resources on web I found for now did not actually deal with threads and forms.
I encountered the below issues:
a. how to work with threads and forms - what I am seeing is that most of the time I am opening a new execution window the cursor (SynMemo caret) does not move to the new window even if I am focusing it most probably because of some threads issues
b. how to understand if a thread from (sub)form N is still executing and how to stop it
c. if I want to close the software how I could find all the threads running from my N (sub)forms
d. how should I close the browser window which is a subform of the execution window which is a subform of the main window.

Please advise if a small tutorial on threads and forms could be presented, I am placing some code below for the closing of sub-sub-forms maybe this could be considered also.

For d.:

Code: Select all

procedure TFrmMain.FormCreate(Sender: TObject);
begin
  fContainerList := TObjectList.create(true);     //initializing
end;
 
  (* adding a execution window *)
  fContainerList.Add(w); 
 
  (* adding a browser window *)
  fbrowser := TTblList.Create(self);    //the self is actually w from above
 
  (* what about closing? would the below free the execution forms  what about the browser forms? *)
  fContainerList.Free;


Thank you,

sysrpl
Posts: 108
Joined: Thu Feb 05, 2015 6:31 pm

Re: Working with threads and forms

Postby sysrpl » Mon Apr 06, 2015 7:12 pm

tudi_x,

A good rule of thumb with regards to Lazarus and threads is not never touch a user interface element from any thread other than the main user interface thread. This means don't update a grid or update a data source which is connected to a grid while inside a thread.

So then you might be asking how can you ensure when some code is executing that it is executing in the main user interface thread?

The answer is simple, use the thread Synchronize method. Synchronize always executes in the context of the main user interface thread. Here's an example:

unit Unit1;

{$mode delphi}

interface

uses
Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls;

{ TForm1 }

type
TForm1 = class(TForm)
Button1: TButton;
{ Button1Click starts work in a background thread }
procedure Button1Click(Sender: TObject);
{ FormCloseQuery prevents the form from being closed if a thread is running }
procedure FormCloseQuery(Sender: TObject; var CanClose: boolean);
private
{ Note that it's not important the form know what type of threads are being used }
FThread: TThread;
end;

var
Form1: TForm1;

implementation

{$R *.lfm}

{ TExampleThread }

type
TExampleThread = class(TThread)
private
{ Store a reference to a form here }
FForm: TForm1;
{ You could optionally store your dataset here }
{ FQuery: TQuery }
{ Complete is synchronized with the main user interface thread }
procedure Complete;
protected
{ Execute runs in a background thread }
procedure Execute; override;
public
{ Execute runs in a background thread }
constructor Create(Form: TForm1);
end;

{ TExampleThread }

constructor TExampleThread.Create(Form: TForm1);
begin
{ Store a copy of which form instance we are associated }
FForm := Form;
{ It is still safe to modify user interface elements here }
FForm.Caption := 'Thread is running';
{ Start running right away }
inherited Create(False);
end;

procedure TExampleThread.Complete;
begin
{ By settings form thread variable to nil we signal we are done }
FForm.FThread := nil;
{ If you need to update user interface elements like a grid, do it here }
FForm.Caption := 'Thread has completed';
{ You could connect the result of FQuery to a grid here }
end;

procedure TExampleThread.Execute;
begin
{ Free the thread when it completes }
FreeOnTerminate := True;
{ Do your work here, which may include database activity.
We simulate some work here by issuing a sleep for 10 seconds command. }
Sleep(10000);
{ You could use FQuery here }
{ Then we signal the main user interface we are done }
Synchronize(Complete);
end;

{ TForm1 }

procedure TForm1.Button1Click(Sender: TObject);
begin
{ If work in a thread hasn't been started yet then start it here }
if FThread = nil then
FThread := TExampleThread.Create(Self);
end;

procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: boolean);
begin
{ Dont allow the form to close if there is still ongoing work in a thread }
if FThread <> nil then
begin
{ This stops the form from being closed and destroyed }
CanClose := False;
{ Tell the user we're still doing some work and wait for it to complete before closing }
ShowMessage('Cannot close while work is being performed');
end;
end;

end.

Regarding stopping executing threads, what you do is this, call FThread.Terminate to signal to the thread that it should stop, then inside you execute periodically check Terminated. When Termianted is true exit from the Execute method.

procedure TExampleThread.Stopped;
begin
{ By settings form thread variable to nil we signal we are stopped }
FForm.FThread := nil;
{ Do other work here on stop where we might need to touch the user interface. }
{ Here for example we close the form. }
FForm.Close;
end;

function TExampleThread.CanContinue: Boolean;
begin
{ Allow stopped to be invoked only one time }
if FStopped then
Exit(False);
{ The thread is stopped if it's terminated prematurely }
Result := not Terminated;
if not Result then
begin
{ Set FStopped to true }
FStopped := True;
{ And invoke the Stopped method in the context of the main user interface thread }
Synchronize(Stopped);
end;
end;

procedure TExampleThread.Execute;
begin
{ Free the thread when it completes }
FreeOnTerminate := True;
{ Do your work here }
if CanContinue then
DoWorkA
else
Exit;
if CanContinue then
DoWorkB
else
Exit;
if CanContinue then
DoWorkC
else
Exit;
{ Then we signal the main user interface we finished without being stopped }
Synchronize(Complete);
end;

In cases where units of work (DoWorkA, DoWorkB) are long running (a complex query) if you want to terminate them early you'll need to break the query down into smaller pieces and run each piece separately. This is actually the smart thing to do because if you've got a long running create, update or delete, cancelling the query prematurely can cost quite a bit as a database rollback operation might be triggered.

For example if you have a query which updates 5 million records and takes 10 minutes to execute, cancelling the query after 9 minute might cause 4.5 million records to be rolled back tying the database up for even longer. Instead you might consider updating records in blocks of 100, and check CanContinue after each block.


Return to “Getting Started”

Who is online

Users browsing this forum: No registered users and 1 guest