2011. június 1., szerda

Simple Thread Example


Problem/Question/Abstract:

This article will show you how to create Threads and show you how to work with global variables within a Thread. You need the unit SyncObjs (part of Delphi Enterprise) for this sample.

Answer:

As mentioned in the Abstract, you will need the unit SyncObjs. This unit, however, is a Delphi Enterprise feature. In another article I will show you how to work around this problem, however, until then you may search the web for workarounds. There are some available already.

THREADS

Threads will allow you to open up one or more additional processes within your application. This allows you to process different tasks parallel. Usually your Delphi applications will accomplish one task after another.

Operationg systems like Windows NT or Windows 2000, however, support multi-tasking, allowing multiple processes to work at virtualyl the same time. Within you applications you can make use of these feature by using threads. This will come in handy in different cases like multiple-processor machines or if one task has to wait for something else (e.q. disk access). A single threaded application (e.q. your normal Delphi app) will have to wait until every task is accomplished before running the next one - mutli-threaded apps work them parallel.

WHEN TO USE THREADS

Either you want to support multi-processor machines or you know your tasks depend on other tasks and not only on your calculations.

Especially on single processor machines the use of threads may actually degrade the performance of your whole application. If some task(s) have to be accomplished before the application can continue and all of these tasks are rather demanding for the processor you may consider not to use threading. However, on a multi-processor machine you will, almost certainly, gain speed using threading.

You should use threading when:

multiple, independent tasks need to be accomplished
your application runs on multi-processor systems
the accomplished tasks run idle some of their working time
you want to learn working with Threads :)

PROBLEMS WITH THREADS

Using Threads can open little "trouble cans." In single-threaded apps you have control over the execution order of your tasks and the way they access global variables. In multi-threaded applications you do not have this kind of control anymore, because multiple threads can access the variable at the same time. Reading global variables out of a thread will, usually, not create problems. Writing, itself, is no problem either. However, reading a variable, working with it and writing the new value back to the variable, will certainly bring you in trouble if two threads do this at the same time.

NOTE: For all of you how "hate" global variables, I do too. However, in threaded applications you will often have to use them in order to exchange processing informations.

CRITICAL SECTIONS

The problem mentioned before is the "critical section" of your thread. Once you decide to access a global variable, work with it, change its value and write it back you have to ensure that no other thread will do the same with this variable at the same time. Windows offers CriticalSections as a mean of thread control. Only one thread at a time can be within the critical section. Therefore, only one thread at a time can manipulate the value of the variables.

Critical Sections are not depending on their position in the code. You can use on Critical Section for different areas of your application. A critical section is a specific variable that holds references for every thread accessing in, allowing only one thread at a time to pass through it. Therefore, a thread should only use critical sections where needed and release it as soon as possible. Additionally, you have to ensure that the thread will leave the critical section or no other thread will be able to enter it - your application will not continue processing any data.

Pseudo-Code without a CS
Pseudo-Code with a CS
-
-
Load global variable
-
Work with gl. var.
-
Save global variable
-
-
-
Enter CS
try
   Load global variable
   -
   Work with gl. var.
   -
   Save global variable
finally
   Leave CS
end



NOTE: The use of critical sections will, slightly, slow down you application because of the processing (Enter/Leave) of the critical section as well as the fact that only one process can be within the code area of the critical section.

ENOUGH THEORY - A SAMPLE

The following sample will not be "great," it will be simple to show you the facts addressed before. The use of the variables isn't the best, however, do not mind - it just simple to learn.

THE FORM

Create a new application. Name the Form frmMain. To the form add an SpinEdit (sedtThreadCnt) from the Samples page. There we can choose how many threads will be started from our application. Add a Check Box (chkCS) allowing the user to choose whether the thread-safe (with a critical section) model is used or not. Add a Label (lblResult) to show the final Result of our Threads and a Button (btnStart) allowing the us to start the Thread Test. For the Form add an OnCreate and an OnDestroy, for the Button an OnClick event using the Object Inspector.

The code snippet below shows the full declaration of the form. Adapt the private and the public section.

NOTE: Add the unit "SyncObjs" to your global uses clause - it is needed for the TCriticalSection class.

TfrmMain = class(TForm)
  Label1: TLabel;
  sedtThreadCnt: TSpinEdit;
  btnStart: TButton;
  lblResult: TLabel;
  chkCS: TCheckBox;
  procedure btnStartClick(Sender: TObject);
  procedure FormCreate(Sender: TObject);
  procedure FormDestroy(Sender: TObject);
private
  { Private declarations }
  FThreadCount: Integer;
  FCriticalSection: TCriticalSection;
  FGlobalVariable: Integer;
  procedure ThreadDone(Sender: TObject);
  procedure SetGlobalVariable(const Value: Integer);
public
  { Public declarations }
  property GlobalVariable: Integer
    read FGlobalVariable
    write SetGlobalVariable;
  property CriticalSection: TCriticalSection
    read FCriticalSection;
end;

THE THREAD CLASSES

The following code snippet shows the declarations of both the unsafe and the safe version. Besides the class names they ar identical.

Every running Thread will increment a global variable 1000 times by one. Therefore, after running exactly one thread the global variable should be 1000, after 2 threads 2000, after 3 threads 3000, and so on ... or ? Well depending on the use of the critical section - one thread model will return the result as expected the other will not...

TUnsafeSampleThread = class(TThread)
private
  FLocalVariable: Integer;
protected
public
  procedure Execute; override;
end;

TSafeSampleThread = class(TThread)
private
  FLocalVariable: Integer;
protected
public
  procedure Execute; override;
end;

THE FULL SOURCE CODE

Below you can see the full source code. One, not nice part, is the Execute part of both Thread versions. You will see quite often the line:

Application.ProcessMessages;

I had to add this line in order to allow the other threads to execute as well. Our way of adding "idle time" to the threads.

RUNNING THE APPLICATION

The application will allow you to choose the number of threads running concurrently and whether to use the safe version or not. Have fun...

unit uMainForm;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls, Spin, SyncObjs;

type
  TUnsafeSampleThread = class(TThread)
  private
    FLocalVariable: Integer;
  protected
  public
    procedure Execute; override;
  end;

  TSafeSampleThread = class(TThread)
  private
    FLocalVariable: Integer;
  protected
  public
    procedure Execute; override;
  end;

  TfrmMain = class(TForm)
    Label1: TLabel;
    sedtThreadCnt: TSpinEdit;
    btnStart: TButton;
    lblResult: TLabel;
    chkCS: TCheckBox;
    procedure btnStartClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    { Private declarations }
    FThreadCount: Integer;
    FCriticalSection: TCriticalSection;
    FGlobalVariable: Integer;
    procedure ThreadDone(Sender: TObject);
    procedure SetGlobalVariable(const Value: Integer);
  public
    { Public declarations }
    property GlobalVariable: Integer
      read FGlobalVariable
      write SetGlobalVariable;
    property CriticalSection: TCriticalSection
      read FCriticalSection;
  end;

var
  frmMain: TfrmMain;

implementation

{$R *.DFM}

{ TUnsafeSampleThread }

procedure TUnsafeSampleThread.Execute;
var
  I: Integer;
begin
  for I := 1 to 1000 do
  begin
    Application.ProcessMessages;
    FLocalVariable := frmMain.GlobalVariable;
    Application.ProcessMessages;
    Inc(FLocalVariable);
    Application.ProcessMessages;
    frmMain.GlobalVariable := FLocalVariable;
  end;
end;

{ TSafeSampleThread }

procedure TSafeSampleThread.Execute;
var
  I: Integer;
begin
  for I := 1 to 1000 do
  begin
    Application.ProcessMessages;
    frmMain.CriticalSection.Acquire;
    try
      FLocalVariable := frmMain.GlobalVariable;
      Application.ProcessMessages;
      Inc(FLocalVariable);
      Application.ProcessMessages;
      frmMain.GlobalVariable := FLocalVariable;
    finally
      frmMain.CriticalSection.Release;
    end;
  end;
end;

{ TfrmMain }

procedure TfrmMain.ThreadDone(Sender: TObject);
begin
  Dec(FThreadCount);
  if FThreadCount = 0 then
  begin
    btnStart.Enabled := True;
    lblResult.Caption := 'GlobalVariable: ' + IntToStr(GlobalVariable);
  end;
end;

procedure TfrmMain.btnStartClick(Sender: TObject);
var
  I: Integer;
begin
  GlobalVariable := 0;
  FThreadCount := sedtThreadCnt.Value;
  for I := 0 to FThreadCount - 1 do
    if chkCS.Checked then
      with TSafeSampleThread.Create(False) do
        OnTerminate := ThreadDone
    else
      with TUnsafeSampleThread.Create(False) do
        OnTerminate := ThreadDone;
  btnStart.Enabled := False;
end;

procedure TfrmMain.SetGlobalVariable(const Value: Integer);
begin
  FGlobalVariable := Value;
end;

procedure TfrmMain.FormCreate(Sender: TObject);
begin
  FCriticalSection := TCriticalSection.Create;
end;

procedure TfrmMain.FormDestroy(Sender: TObject);
begin
  FCriticalSection.Free;
end;

end.

3 megjegyzés:

  1. FLocalVariable := frmMain.GlobalVariable; gives me 0, but I set global variable before the assign.

    VálaszTörlés
  2. That'S Perfect! All at once it became clear. Author of the post thank you very much. http://progfromdelphi.com/

    VálaszTörlés
  3. Was looking for this stuff but found only the ego on your site. More precisely and concisely than you none the material is presented.

    VálaszTörlés