Delphi threading by example
An example with the Windows API
MJ
By Wim De Cleen, Software Engineer with Electromatic Sint-Niklaas NV (Belgium)
Threads are somewhat overwhelming until you have written some threading code and gotten experience with their ins and outs. This article will introduce you to the art of threading and synchronization. I use the Windows API calls to show you how it really works -- the hard truth. I wrote an example that searches for a specific string in multiple files. As the threads search, real-time synchronized information is sent to the main form. Of course, the data could also be sendtto another thread or application. The more you think about it, the more you see the power of threading.
Why synchronize?
When a thread is running, it runs on its own without wondering what other threads are doing. That is why one thread can calculate a sum of two numbers -- let's say a and b, while another thread comes to life and changes the value of a. This is not what we want! So we need a shell around the global variables so that no other thread can access them. Then, when the calculations are done, we might remove the shell to give other threads in the system access to the data once more.
The easiest way to construct such a shell is to use critical sections. We will use these as a start.
(Incidentally, synchronization bugs like this can bite you when you are using VCL components, which are generally not thread-safe. When you start updating controls from different threads everything may seem fine...but the bug will present itself some time in the future. Then it is up to you to find the synchronization bug. Believe me, this can take a while. Debugging threads is really frustrating!
Getting started
Delphi's TThread class has a method called synchronize, and if it did everything we wanted then there would be no need for this article. TThread lets you specify a method that needs to be synchronized with the main thread (the thread of the VCL). This is handy if you can afford to spend time waiting for synchronization, but in systems where milliseconds are crucial you can't use this method: It halts the execution of the thread during synchronization.
My alternative method is based on messages. With the function PostMessage you can deliver messages to any thread in the system without waiting for the result. The message is just placed onto the message queue of the receiving thread and stays there until the receiving thread comes to life. At that time the message is handled. All that time your thread continues to run.
Critical sections
Critical sections are the simplest synchronizers possible. They are fast because they can be used only within the same process. A critical section can be used to protect global variables but you must perform the coordination yourself. You can't bind a variable to a critical section programmatically; it must be done logically.
Let's say we want to protect any access to the variables a and b. Here's how to do it with the critical sections approach:
//global variables
var CriticalSection: TRTLCriticalSection;
a, b: integer;
//before the threads starts
InitializeCriticalSection(CriticalSection);
//in the thread
EnterCriticalSection(CriticalSection);
//From now on, you can safely make
//changes to the variables.
inc(a);
inc(b);
//End of safe block
LeaveCriticalSection(CriticalSection);
See? Nothing to it. This approach lets you create multiple threads without worrying about the effect on global variables.
Starting the thread
When creating a thread without the TThread class, always use the BeginThread function from the SysUtils unit. It is specifically written to use Pascal functions and it encapsulates the CreateThread winapi call.
Let's take a look at the declaration and step through the parameters.
BeginThread(SecurityAttributes: Pointer;
StackSize: LongWord;
ThreadFunc: TThreadFunc;
Parameter: Pointer;
CreationFlags: LongWord;
var ThreadId: LongWord): Integer;
-
SecurityAttributes: a pointer to a security record, used only in windows NT, fill in nil
-
StackSize: the initial stack size of the thread. The default value is 1MB. If you think this is too small fill in the desired size, otherwise if not fill in 0.
-
ThreadFunc: This is the function that will be executed while the thread is running. This is mostly a function with a while loop inside. The prototype is function(Parameter: Pointer): Integer
-
Parameter: This is a pointer to a parameter, which can be anything you like. It will be passed to the thread function as the parameter pointer.
-
CreationFlags: This flag determines whether the thread starts immediately or it is suspended until ResumeThread is called. Use CREATE_SUSPENDED for suspended and 0 for an immediate start.
-
ThreadID: This is a var parameter that will return the ThreadID of the thread.
Handling the messages
Handling messages sent to a form is simple in Delphi. You just have to declare a procedure with the following prototype: procedure(var Message:TMessage). This procedure must be declared as a method of your form class. By using the message directive you bind a message to the function. In the form declaration it looks like this:
const
TH_MESSAGE = WM_USER + 1;
type
Form1 = class(TForm)
privateprocedure HandleMessage(var Message:
TMessage); message TH_MESSAGE;
publicend;
From now on every message sent to the form with the message parameter TH_MESSAGE will invoke the HandleMessage procedure.
We can send a message with the PostMessage function which looks like the following"
PostMessage (Handle: HWND; Message: Cardinal; WParam: integer; LParam:integer)
-
Handle: The handle of the receiving thread or receiving form.
-
Message: The messageconstant.
-
WParam: An optional parameter.
-
LParam: An optional parameter.
Putting things together.
The example program uses messages to deliver information to the main form from within threads.
First I declared the message constants and the submessage constants. Submessages will be passed through the LParam.
const
TH_MESSAGE = WM_USER + 1; //Thread message
TH_FINISHED = 1; //Thread SubMessage
//End of thread
//WParam = ThreadID
TH_NEWFILE = 2; //Thread Submessage
//Started new file
TH_FOUND = 3; //Thread Submessage
//Found search string in File
TH_ERROR = 4; //Thread SubMessage
//Error -- WParam = Error
Then the information records, which will be passed through WParam:
type
//Record for found items, will occur when
//LParam = TH_FOUND, WParam will be
//PFoundRecord
PFoundRecord = ^TFoundRecord;
TFoundRecord = record
ThreadID: Cardinal;
Filename: string;
Position: Cardinal;
end;
//Record for new files, will occur when
//LParam = TH_NEWFILE, WParam will be
//PNewFileRecord
PNewFileRecord = ^TNewFileRecord;
TNewFileRecord = record
ThreadID: Cardinal;
Filename: string;
end;
Since we need some information about the threads we declare an info record for them:
//Record to hold the information from one thread
TThreadInfo = record
Active: Boolean;
ThreadHandle: integer;
ThreadId: Cardinal;
CurrentFile: string;
end;
Finally we declare the TMainForm class, which contains the message handler:
//The Main form of the application
TMainForm = class(TForm)
btSearch: TButton;
Memo: TMemo;
OpenDialog: TOpenDialog;
edSearch: TEdit;
StringGrid: TStringGrid;
Label1: TLabel;
procedure btSearchClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure edSearchChange(Sender: TObject);
private
ThreadInfo: array[0..4] of TThreadInfo;
//Holds the information of the threads
procedure ThreadMessage(var Message: TMessage);
message TH_MESSAGE; //MessageHandler
function ThreadIDToIndex(ThreadID: Cardinal):
integer;
publicend;
In the implementation section we declare our global variables, the critical section, a search string, and the list of files we will be searching.
var CriticalSection: TRTLCriticalSection;
//Critical section protects the filelist
FileList: TStringList;
//List of filenames to be searched
SearchString: string;
//String to be searched in every file
Then follows the thread function, this is the function that delivers all the work for the thread. It will be passed to the begin thread function.
function FindInFile(data: Pointer): Integer;
var FileStream: TFileStream;
Ch: char;
Current,Len: Integer;
FoundRecord: PFoundRecord;
NewFileRecord: PNewFileRecord;
Filename: string;
Search: string;
FilesDone: Boolean;
begin
Result:= 0;
FilesDone:= False;
whilenot FilesDone dobegin
Current:= 1;
EnterCriticalSection(CriticalSection);
//Try to catch the critical section
Search:= SearchString;
//Access the shared variables
//Are there still files available
if FileList.Count = 0 thenbegin
//Leave the critical section
//when there are no files left
LeaveCriticalSection(CriticalSection);
//Leave the while loop
break;
endelsebegin
//Read the filename
Filename:= FileList.Strings[0];
//Delete the file from the list
FileList.Delete(0);
//Leave the critical section
LeaveCriticalSection(CriticalSection);
//Inform MainForm of New File
New(NewFileRecord);
NewFileRecord^.Filename:= Filename;
NewFileRecord^.ThreadID:= GetCurrentThreadID;
PostMessage(MainForm.Handle,
TH_MESSAGE, TH_NEWFILE,
Integer(NewFileRecord));
Len:= Length(Search);
try
FileStream:= TFileStream.Create(
Filename, fmOpenRead or fmShareExclusive);
except
PostMessage(MainForm.Handle, TH_MESSAGE,
TH_ERROR, ERROR_COULD_NOT_OPEN_FILE);
continue;
end;
//The search algorithm, pretty simple,
//the example is not about searching
while FileStream.Read(Ch,1)= 1 dobeginIf Ch = Search[Current] thenbegin
Inc(Current);
if Current > Len thenbegin
//Found the search string,
//inform MainForm of our success
New(FoundRecord);
FoundRecord^.Filename:= Filename;
FoundRecord^.Position:= FileStream.Position;
FoundRecord^.ThreadID:= GetCurrentThreadID;
PostMessage(MainForm.Handle,
TH_MESSAGE, TH_FOUND,
Integer(FoundRecord));
end;
endelsebegin
FileStream.Position:=
FileStream.Position - (Current - 1);
Current:= 1;
end;
end;
FileStream.Free;
end;
end;
//All done inform MainForm of ending
PostMessage(MainForm.Handle,
TH_MESSAGE, TH_FINISHED, GetCurrentThreadID);
end;
Another important procedure is the message handler, which -- drum roll, please -- handles the messages. It also frees the record pointers.
procedure TMainForm.ThreadMessage(var Message: TMessage);
var FoundRecord: PFoundRecord;
NewFileRecord: PNewFileRecord;
ThreadIndex: integer;
Counter: integer;
Ended: boolean;
begincase Message.WParam of
TH_FINISHED:
begin
ThreadIndex:= ThreadIDToIndex(Message.LParam);
if ThreadIndex = -1 then Exit;
//Invalid threadID should never appear
CloseHandle(ThreadInfo[ThreadIndex].ThreadHandle);
//Free the thread memoryspace
StringGrid.Cells[3,ThreadIndex+1]:= 'False';
//Update the stringgrid
Threadinfo[ThreadIndex].Active:= False;
//Update the ThreadInfo array
Ended:= True;
for counter:= 0 to 4 doif ThreadInfo[ThreadIndex].Active thenbegin
Ended:= False;
break;
end;
if Ended then btSearch.Enabled:= True;
end;
TH_NEWFILE:
begin
NewFileRecord:= PNewFileRecord(Message.LParam);
ThreadIndex:= ThreadIDToIndex
(NewFileRecord^.ThreadID);
if ThreadIndex = -1 then Exit;
//Invalid threadID should never appear
StringGrid.Cells[2,ThreadIndex+1]:=
NewFileRecord^.Filename; //Update StringGrid
ThreadInfo[ThreadIndex].CurrentFile:=
NewFileRecord^.Filename; //Update ThreadInfo
Dispose(NewFileRecord);
//All information is used now free the pointer
end;
TH_FOUND:
begin
FoundRecord:= PFoundRecord(Message.LParam);
ThreadIndex:= ThreadIDToIndex(FoundRecord^.ThreadID);
if ThreadIndex = -1 then Exit;
//Invalid threadID should never appear
Memo.Lines.Add(FoundRecord^.Filename + ' Position:'
+ IntToStr(FoundRecord^.Position));
Dispose(FoundRecord);
//All information is used now free the pointer
end;
TH_ERROR:
begin
ThreadIndex:= ThreadIDToIndex(Message.LParam);
if ThreadIndex = -1 then Exit;
//Invalid threadID should never appear
Memo.Lines.Add('Error: Could not open file '
+ ThreadInfo[ThreadIndex].CurrentFile);
end;
end;
end;
You don't need to worry about pasting the functions and classes together. I have included the complete
example
in a Zip file so you can see it working right away.
This is not a complete explanation of threading. It's barely a beginning. But I hope it will serve as a starting point for your own exploration. I wish I had had an example to guide me when I started working with threads!