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!