Multi Threading in Delphi

What are threads? and why we use them?
Earlier days of programming all programs were single threaded and single tasking in which your program will ran exclusively on the machine or not at all. With increasingly sophisticated applications and increasing demands on personal computers, now multiprocessing and multithreading operating systems are available. Multithreading on computer programming was mainly required for better performance and usability.

So first lets go about Process, Process is a program that runs on a system and uses system resources like CPU, Memory etc. And every process has a main thread. In a Process many actions can be performed one by one on fast in fast perform order. And Thread is generally used to perform several set of actions at once in situations like some actions may cause a considerable delay but during that period the program should be able to perform other actions too. For an example in windows explorer we are copying a large volume of data from one folder to another folder and we found it will take long time but during copy we can do other work also. So in this case copy process is going on in a new thread which is not effecting to main thread.

Threads are mostly used in case of performance related problems. Here are some examples where we might use threads.
   - Doing lengthy processing : When a windows application is calculating it cannot process any more messages. As a result, the display cannot be updated.
   - Doing background processing : Some tasks may not be time critical, but need to execute continuously.
  - Doing I/O work : I/O to disk or to network can have unpredictable delays. Threads allow you to ensure that I/O operation should not delay unrelated parts of your application.

Creating a Thread in Delphi
Before creating a separate Thread, lets get some idea about VCL main thread in Delphi application. Actually every Delphi application has main thread which executes when the application starts and terminates when application exits. And during application run when we don't do anything then main thread suspends for some time and when user again do some action main thread resumes and starts executing the actions.

Following image shows how VCL main thread executes during application run.

To create and execute a separate Thread in application, Delphi provides TThread class which is an abstract class. It provides options for creating threads, executing threads and terminate when required. How ever we cannot create TThread class objects directly as it is an abstract class. So we have to create a new class by inheriting from this class and need to implement Execute method. Then we can create the object of this class to create a new thread. Then we can start a thread to perform some other actions. And a Thread will terminate automatically or we can terminate manually as per required.

Lets see an example of thread which find list of prime numbers till give number and save into a file...
type
  TPrimeThrd = class(TThread)
  private
    FToNumber: integer;
  protected
    function IsPrime(iNum: integer): boolean;
    procedure Execute; override;
  public
    property ToNumber: integer write FToNumber;
  end;
.......
function TPrimeThrd.IsPrime(iNum: integer): boolean;
var
  iCnt: integer;
begin
  result := true;
  if iNum < 0 then
  begin
    result := false;
    exit;
  end;
  if iNum <= 2 then
    exit;
  for iCnt := 2 to iNum - 1 do
  begin
    if (iNum mod iCnt) = 0 then
    begin
      result := false;
      exit;
    end;
  end;
end;
procedure TPrimeThrd.Execute;
var
  iCnt: integer;
  slPrime: TStringlist;
begin
  Try
    slPrime := TStringlist.Create(Nil);
slPrime.Clear;
for iCnt :=  0 to FToNumber-1 do
begin
 if IsPrime(iCnt) then
 begin
   slPrime.Add(inttostr(iCnt));
 end;
end;
slPrime.SavetoFile('C:\Prime1.txt');
  finally
    slPrime.Free;  
  end;
end;
On above code TPrimeThrd class inheriting from TThread class. ToNumber is property used to pass a number till that we will find the prime numbers. IsPrime method to check a number is prime or not. We have to override Execute method as TThread class is abstract class. And Execute method contains actual codes that will execute when a Thread starts.


Lets see how to use above TPrimeThrd objects to start a new Thread...
type
  TPrimeFrm = class(TForm)
    NumEdit: TEdit;
    SpawnButton: TButton;
    procedure SpawnButtonClick(Sender: TObject);
  private
      { Private declarations }
  public
      { Public declarations }
  end;
......
procedure TPrimeFrm.SpawnButtonClick(Sender: TObject);
var
  NewThread: TPrimeThrd;
begin
  NewThread := TPrimeThrd.Create(True);
  NewThread.FreeOnTerminate := True;
  try
    NewThread.ToNumber := StrToInt(NumEdit.Text);
    NewThread.Resume;
  except on EConvertError do
    begin
      NewThread.Free;
      ShowMessage('That is not a valid number!');
    end;
  end;
end;
On above code each time the "Spawn" button is clicked, the program creates a new NewThread as TPrimeThrd object. We have passed True as parameter to TPrimeThrd.Create, this means thread object will be created as suspended and thread will not start till we call Resume or Start method. If we pass False means Thread will be created and will start immediately. FreeonTerminate defines that how Thread object will free. If FreeeonTerminate is True then Thread object will free automatically once executed successfully means after statements written in Execute method runs successfully. And on above code after create thread object we have we have assigned ToNumber property so that thread will find prime numbers from 1 to till that number. And Resume method will start a suspended thread. So once Resume method called Thread object will start and TPrimeThrd.Execute will be called. And if thread runs successfully it will free thread object automatically as FreeOnTerminate=True. If any error occurred then NewThread.Free will free the thread
object.

Following Image explains how multi thread works

Delphi Threads Communication and Synchronization 
TThread.Synchronize
Sometime we have more than one thread and we need to communicate between them to access shared resources. Suppose two threads accessing a shared integer variable can result in complete disaster, and un-synchronized access to shared resources or VCL calls will result in many hours of fraught debugging. So we have to protect all the shared resources with secure communication. if we do not have adequate synchronization then we may not able to do followings...
- Accessing any form of shared resource between two threads.
- Playing with thread unsafe parts of the VCL in a non-VCL thread.
- Attempting to do graphics operations in a separate thread.
For this issue Delphi provides Synchronize method which takes as a parameter a parameter less method which you want to be executed. You are then guaranteed that the code in the parameter less method will be executed as a result of the synchronize call, and will not conflict with the VCL thread. Code which is invoked when synchronize is called can perform anything that the main VCL thread might do and can also modify data associated with its own thread object

For example We will modify our prime number program, so that instead of showing a message box, it indicates whether a number is prime or not by adding some text to a memo in the main form. First of all, we'll add a new memo (ResultsMemo) to our main form.
type
  TPrimeFrm = class(TForm)
    NumEdit: TEdit;
    SpawnButton: TButton;
    ResultsMemo: TMemo;
    procedure SpawnButtonClick(Sender: TObject);
  private
      { Private declarations }
  public
      { Public declarations }
  end;
We add another method (UpdateResults) to our thread which displays the results on the memo instead of storing in a TStringlist and then saving into a file. We call Synchronize, passing this method as a parameter. Here UpdateResults accesses both the main form and a result string.
type
  TPrimeThrd = class(TThread)
  private
    FToNumber: integer;
    FResultString: string;
  protected
    function IsPrime: boolean;
    procedure UpdateResults;
    procedure Execute; override;
  public
    property ToNumber: integer write FToNumber;
  end;
........
procedure TPrimeThrd.UpdateResults;
begin
  PrimeFrm.ResultsMemo.Lines.Add(FResultString);
end;
........
procedure TPrimeThrd.Execute;
var
  iCnt: integer;
begin
  for iCnt :=  0 to FToNumber-1 do
  begin
    if IsPrime(iCnt) then
begin
 FResultString := inttostr(iCnt);
 Synchronize(UpdateResults);
end;
  end;
end;

Fetching data from a database by a Thread in Delphi
When we required to fetch data from a database like import or export functionality we can use threads for better solution. In this case each threads
should have their own data components like Connection, Query etc. We should create all Components and set required properties like Conenctionstring, SQL at run time. We should Open and close connections at run time only. For example I will execute a thread which will import some data from CONTACT table from a Oracle DB for Import. This example will also update user number of records imported.
Import Thread...
type
  TImport = class(TThread)
  private
    CDImportData : TClientDataSet;
    connMain : TAdoConnection;
    qryImport : TADOQuery;
    iImportRecord,iImpRecCount : Integer;
    procedure ImportData;
    procedure UpdateImportUI;
    procedure UISettingsEnable;
    procedure UISettingsDisable;
  public
    procedure Execute;Override;
end;
........
procedure TImport.Execute;
begin
  inherited;
  CoInitialize(nil);
  ImportData;
end;
procedure TImport.ImportData;
var
  bIsRecExist : Boolean;
begin
  try
    iImportRecord := 0;
    iImpRecCount := 0;
    bIsRecExist := False;
connMain := TAdoConnection.Create(Nil);
connMain.LoginPrompt := False;
connMain.ConnectionString := 'Provider=OraOLEDB.Oracle.1;Password=pass;Persist Security Info=True;User ID=user;Data Source=yoursource;Extended Properties=""';
    connMain.Open;
qryImport := TADOQuery.Create(nil);
    qryImport.Close;
    qryImport.Connection := connMain;
    qryImport.SQL.Text := ' SELECT C.ID,C.FIRST_NAME,C.EMAIL '+
                          ' FROM CONTACT C '+
                          ' WHERE C.FIRST_NAME IS NOT NULL ';
    qryImport.Open;
    CDImportData := TClientDataSet.Create(nil);
    CDImportData.FieldDefs.Add('Id',ftString,10,True);
    CDImportData.FieldDefs.Add('FIRST_NAME',ftString,50,True);
    CDImportData.FieldDefs.Add('EMAIL',ftString,50,False);
    CDImportData.CreateDataSet;
    CDImportData.Active := True;
    CDImportData.Open;
    if qryImport.RecordCount > 0 then
    begin
      bIsRecExist := True;
      iImpRecCount := qryImport.RecordCount;
      Synchronize(UISettingsEnable);
      qryImport.First;
      while not qryImport.Eof do
      begin
        CDImportData.Append;
        CDImportData.FieldByName('Id').AsString := qryImport.FieldByName('Id').AsString;
        CDImportData.FieldByName('FIRST_NAME').AsString := qryImport.FieldByName('FIRST_NAME').AsString;
        CDImportData.FieldByName('EMAIL').AsString := qryImport.FieldByName('EMAIL').AsString;
        CDImportData.Post;
        qryImport.Next;
        iImportRecord := iImportRecord + 1;
        Synchronize(UpdateImportUI);
      end;
    end;
    if CDImportData.RecordCount > 0 then
      CDImportData.SaveToFile('C:\MYIMPORT.CDS');
  finally
    if bIsRecExist then
      Synchronize(UISettingsDisable);
    qryImport.Close;
    FreeAndNil(qryImport)
connMain.Close;
FreeAndNil(connMain)
    CDImportData.Close;
FreeAndNil(CDImportData)
  end;
end;
procedure TImport.UISettingsDisable;
begin
  frmMain.lblIndicate.Visible := False;
  frmMain.pbar.Visible := False;
  frmMain.btnImport.Enabled := True;
end;
procedure TImport.UISettingsEnable;
begin
  frmMain.lblIndicate.Visible := True;
  frmMain.pbar.Visible := True;
  frmMain.btnImport.Enabled := False;
  frmMain.pbar.Max := iImpRecCount;
end;
procedure TImport.UpdateImportUI;
begin
  frmMain.lblIndicate.Caption := 'Importing Data ...   ' +IntToStr(iImportRecord)+ '  Out Of  '+IntToStr(iImpRecCount);
  frmMain.pbar.Position :=  iImportRecord;
end;
Using of above thread to import data in a form having recuired controls...
type
  TfrmMain = class(TForm)
    btnImport: TButton;
    pbar: TProgressBar;
    lblIndicate: TLabel;
    procedure btnImportClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;
..........
procedure TfrmMain.btnImportClick(Sender: TObject);
var
  dImport : TImport;
begin
  dImport := TImport.Create(True);
  dImport.FreeOnTerminate := True;
  dImport.Resume;
end;

Comments

Popular posts from this blog

ShellExecute in Delphi

How to send Email in Delphi?

Variants in Delphi. Use of Variant Array. How to check a Variant is unassigned or empty or clear ?