Simple Thread Example (Views: 2155)
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. |