Articles   Members Online:
-Article/Tip Search
-News Group Search over 21 Million news group articles.
-Delphi/Pascal
-CBuilder/C++
-C#Builder/C#
-JBuilder/Java
-Kylix
Member Area
-Home
-Account Center
-Top 10 NEW!!
-Submit Article/Tip
-Forums Upgraded!!
-My Articles
-Edit Information
-Login/Logout
-Become a Member
-Why sign up!
-Newsletter
-Chat Online!
-Indexes NEW!!
Employment
-Build your resume
-Find a job
-Post a job
-Resume Search
Contacts
-Contacts
-Feedbacks
-Link to us
-Privacy/Disclaimer
Embarcadero
Visit Embarcadero
Embarcadero Community
JEDI
Links
How to Create threads straight from the WinAPI Turn on/off line numbers in source code. Switch to Orginial background IDE or DSP color Comment or reply to this aritlce/tip for discussion. Bookmark this article to my favorite article(s). Print this article
01-Dec-02
Category
Win API
Language
Delphi 2.x
Views
100
User Rating
No Votes
# Votes
0
Replies
0
Publisher:
DSP, Administrator
Reference URL:
DKB
			Author: Lou Adler 

How can I implement threads in my programs without using the VCL TThread object?

Answer:

I've done extensive work in multi-threaded applications. And in my experience, 
there have been times when a particular program I'm writing should be written as a 
multi-threaded application, but using the TThread object just seems like overkill. 
For instance, I write a lot of single function programs; that is, the entire 
functionality (beside the user interface portion) of the program is contained in 
one single execution procedure or function. Usually, this procedure contains a 
looping mechanism (e.g. FOR, WHILE, REPEAT) that operates on a table or an 
incredibly large text file (for me, that's on the order of 500MB-plus!). Since it's 
just a single procedure, using a TThread is just too much work for my preferences.

For those experienced Delphi programmers, you know what happens to the user 
interface when you run a procedure with a loop in it: The application stops 
receiving messages. The most simple way of dealing with this situation is to make a 
call to Application.ProcessMessages within the body of the loop so that the 
application can still receive messages from external sources. And in many, if not 
most, cases, this is a perfectly valid thing to do. However, if some or perhaps 
even one of the steps within the loop take more than a couple of seconds to 
complete processing — as in the case of a query — Application.ProcessMessages is 
practically useless because the application will only receive messages at the time 
the call is made. So what you ultimately achieve is intermittent response at best. 
Using a thread, on the other hand, frees up the interface because the process is 
running completely separate from the main thread of the program where the interface 
resides. So regardless of what you execute within a loop that is running in a 
separate thread, your interface will never get locked up.

Don't confuse the discussion above with multi-threaded user interfaces. What I'm 
talking about is executing long background threads that won't lock up your user 
interface while they run. This is an important distinction to make because it's not 
really recommended to write multi-user interfaces, because each thread that is 
created in the system has its own message queue. Thus, a message loop must be 
created to fetch messages out of the queue so they can be dispatched appropriately. 
The TApplication object that controls the UI would be the natural place to set up 
message loops for background threads, but it's not set up to detect when other 
threads are executed. The gist of all this is that the sole reason you create 
threads is to distribute processing of independent tasks. Since the UI and controls 
are fairly integrated, threads just don't make sense here because in order to make 
the separate threads work together, you have to synchronize them to work in tandem, 
which practically defeats threading altogether!

I mentioned above that the TThread object is overkill for really simple threaded 
stuff. This is strictly an opinion, but experience has made me lean that way. In 
any case, what is the alternative to TThread in Delphi?

The solution isn't so much an alternative as it is going a bit more low-level into 
the Windows API. I've said this several times before: The VCL is essentially one 
giant wrapper around the Windows API and all its complexities. But fortunately for 
us, Delphi provides a very easy way to access lower-level functionality beyond the 
wrapper interface with which it comes. And even more fortunate for us, we can 
create threads using a simple Windows API function called CreateThread to bypass 
the TThread object altogether. As you'll see below, creating threads in this 
fashion is incredibly easy to do.

Setting Yourself Up

There are two distinct steps for creating a thread: 1)Create the thread itself, 
then 2) Provide a function that will act as the thread entry point. The thread 
function or thread entry point is the function (actually the address of the 
function) that tells your thread where to start.

Unlike a regular function, there are some specific requirements regarding the 
thread function that you have to obey:

You can give the function any name you want, but it must be a function name (ie. 
function MyThreadFunc)
The function must have a single formal parameter of type Pointer (I'll discuss this 
below)
The function return type is always LongInt
Its declaration must always be preceded by the stdcall directive. This tells the 
compiler that the function will be passing parameters in the standard Windows 
convention.

Whew! That seems like a lot but it's really not as complicated as it might seem 
from the description above. Here's an example declaration:

1   function MyThreadFunc(Ptr: Pointer): LongInt; stdcall;

That's it! Hope I didn't get you worried. The CreateThread call is a bit more 
involved, but it too is not very complicated once you understand how to call it. 
Here's its declaration, straight out of the help file:
2   
3   function CreateThread
4     (lpThreadAttributes: Pointer; //Address of thread security attributes
5     dwStackSize: DWORD; //Thread stack size
6     lpStartAddress: TFNThreadStartRoutine; //Address of the thread function
7     lpParameter: Pointer; //Input parameter for the thread
8     dwCreationFlags: DWORD; //Creation flags
9     var lpThreadId: DWORD): //ThreadID reference
10  THandle; stdcall; //Function returns a handle to the thread


This is not as complicated as it seems. First of all, you rarely have to set 
security attributes, so that can be set to nil. Secondly, in most cases, your stack 
size can be 0 (actually, I've never found an instance where I have to set this to a 
value higher than zero). You can optionally pass a parameter through the 
lpParameter argument as a pointer to a structure or address of a variable, but I've 
usually opted to use global variables instead (I know, this breaking a cardinal 
rule of structured programming, but it sure eases things). Lastly, I've rarely had 
to set creation flags unless I want my thread to start in a suspended state so I 
can do some preprocessing. For the most part, I set this value as zero.

Now that I've thoroughly confused you, let's look at an example function that 
creates a thread:

11  procedure TForm1.Button1Click(Sender: TObject);
12  var
13    thr: THandle;
14    thrID: DWORD;
15  begin
16    FldName := ListBox1.Items[ListBox1.ItemIndex];
17    thr := CreateThread(nil, 0, @CreateRecID, nil, 0, thrID);
18    if (thr = 0) then
19      ShowMessage('Thread not created');
20  end;


Embarrassingly simple, right? It is. To make the thread in the function above, I 
declared two variables, thr and thrID, which stand for the handle of the thread and 
its identifier, respectively. I set a global variable that the thread function will 
access immediately before the call to CreateThread, then make the declaration, 
assigning the return value of the function to thr and inputting the address of my 
thread function, and the thread ID variable. The rest of the parameters I set to 
nil or 0. Not much to it.

Notice that the procedure that actually makes the call is an OnClick handler for a 
button on a form. You can pretty much create a thread anywhere in your code as long 
as you set up properly. Here's the entire unit code for my program; you can use it 
for a template. This program is actually fairly simple. It adds an incremental 
numeric key value to a table called RecID, based on the record number (which makes 
things really easy). Browse the code; we'll discuss it below:

21  unit main;
22  
23  interface
24  
25  uses
26    Windows, Messages, SysUtils, Classes,
27    Graphics, Controls, Forms, Dialogs, DB, DBTables, StdCtrls, ComCtrls,
28    Buttons;
29  
30  type
31    TForm1 = class(TForm)
32      Edit1: TEdit;
33      Label1: TLabel;
34      OpenDialog1: TOpenDialog;
35      SpeedButton1: TSpeedButton;
36      Label2: TLabel;
37      StatusBar1: TStatusBar;
38      Button1: TButton;
39      ListBox1: TListBox;
40      procedure SpeedButton1Click(Sender: TObject);
41      procedure Button1Click(Sender: TObject);
42    end;
43  
44  var
45    Form1: TForm1;
46    TblName: string;
47    FldName: string;
48  
49  implementation
50  
51  {$R *.DFM}
52  
53  function CreateRecID(P: Pointer): LongInt; stdcall;
54  var
55    tbl: TTable;
56    I: Integer;
57    ses: TSession;
58    msg: string;
59  begin
60    Randomize; //Initialize random number generator
61    I := 0;
62    {Disable the Execute button so another thread can't be executed
63     while this one is running}
64    EnableWindow(Form1.Button1.Handle, False);
65  
66    {If you're going to access any data in a thread, you have to create a
67     separate }
68    ses := TSession.Create(Application);
69    ses.SessionName := 'MyRHSRecIDSession' + IntToStr(Random(1000));
70  
71    tbl := TTable.Create(Application);
72    with tbl do
73    begin
74      Active := False;
75      SessionName := ses.SessionName;
76      DatabaseName := ExtractFilePath(TblName); //TblName is a global variable set
77      TableName := ExtractFileName(TblName); //in the SpeedButton's OnClick handler
78      Open;
79      First;
80      try
81        {Start looping structure}
82        while not EOF do
83        begin
84          if (State <> dsEdit) then
85            Edit;
86          msg := 'Record ' + IntToStr(RecNo) + ' of ' + IntToStr(RecordCount);
87          {Display message in status bar}
88          SendMessage(Form1.StatusBar1.Handle, WM_SETTEXT, 0, LongInt(PChar(msg)));
89          FieldByName(FldName).AsInteger := RecNo;
90          Next;
91        end;
92      finally
93        Free;
94        ses.Free;
95        EnableWindow(Form1.Button1.Handle, True);
96      end;
97    end;
98    msg := 'Operation Complete!';
99    SendMessage(Form1.StatusBar1.Handle, WM_SETTEXT, 0, LongInt(PChar(msg)));
100 end;
101 
102 procedure TForm1.SpeedButton1Click(Sender: TObject);
103 var
104   tbl: TTable;
105   I: Integer;
106 begin
107   with OpenDialog1 do
108     if Execute then
109     begin
110       Edit1.Text := FileName;
111       TblName := FileName;
112       tbl := TTable.Create(Application);
113       with tbl do
114       begin
115         Active := False;
116         DatabaseName := ExtractFilePath(TblName);
117         TableName := ExtractFileName(TblName);
118         Open;
119         LockWindowUpdate(Self.Handle);
120         for I := 0 to FieldCount - 1 do
121         begin
122           ListBox1.Items.Add(Fields[I].FieldName);
123         end;
124         LockWindowUpdate(0);
125         Free;
126       end;
127     end;
128 end;
129 
130 procedure TForm1.Button1Click(Sender: TObject);
131 var
132   thr: THandle;
133   thrID: DWORD;
134 begin
135   FldName := ListBox1.Items[ListBox1.ItemIndex];
136   thr := CreateThread(nil, 0, @CreateRecID, nil, 0, thrID);
137   if (thr = 0) then
138     ShowMessage('Thread not created');
139 end;
140 
141 end.


The most important function here, obviously, is the thread function, CreateRecID. 
Let's take a look at it:
142 
143 function CreateRecID(P: Pointer): LongInt; stdcall;
144 var
145   tbl: TTable;
146   I: Integer;
147   ses: TSession;
148   msg: string;
149 begin
150   Randomize; //Initialize random number generator
151   I := 0;
152   {Disable the Execute button so another thread can't be executed
153    while this one is running}
154   EnableWindow(Form1.Button1.Handle, False);
155 
156   {If you're going to access any data in a thread, you have to create a
157    separate }
158   ses := TSession.Create(Application);
159   ses.SessionName := 'MyRHSRecIDSession' + IntToStr(Random(1000));
160 
161   tbl := TTable.Create(Application);
162   with tbl do
163   begin
164     Active := False;
165     SessionName := ses.SessionName;
166     DatabaseName := ExtractFilePath(TblName); //TblName is a global variable set
167     TableName := ExtractFileName(TblName); //in the SpeedButton's OnClick handler
168     Open;
169     First;
170     try
171       {Start looping structure}
172       while not EOF do
173       begin
174         if (State <> dsEdit) then
175           Edit;
176         msg := 'Record ' + IntToStr(RecNo) + ' of ' + IntToStr(RecordCount);
177         {Display message in status bar}
178         SendMessage(Form1.StatusBar1.Handle, WM_SETTEXT, 0, LongInt(PChar(msg)));
179         FieldByName(FldName).AsInteger := RecNo;
180         Next;
181       end;
182     finally
183       Free;
184       ses.Free;
185       EnableWindow(Form1.Button1.Handle, True);
186     end;
187   end;
188   msg := 'Operation Complete!';
189   SendMessage(Form1.StatusBar1.Handle, WM_SETTEXT, 0, LongInt(PChar(msg)));
190 end;


This is a pretty basic function. I'll leave it up to you to follow the flow of 
execution. However, let's look at some very interesting things that are happening 
in the thread function.

First of all, notice that I created a TSession object before I created the table I 
was going to access. This is to ensure that the program will behave itself with the 
BDE. This is required any time you access a table or other data source from within 
the context of a thread. I've explained this in more detail in another article 
called How Can I Run Queries in Threads? Directly above that, I made a call to the 
Windows API function EnableWindow to disable the button that executes the code. I 
had to do this because since the VCL is not thread-safe, there's no guarantee I'd 
be able to successfully access the button's Enabled property safely. So I had to 
disable it using the Windows API call that performs enabling and disabling of 
controls.

Moving on, notice how I update the caption of a status bar that's on the bottom of 
the my form. First, I set the value of a text variable to the message I want 
displayed:

msg := 'Record ' + IntToStr(RecNo) + ' of ' + IntToStr(RecordCount);

Then I do a SendMessage, sending the WM_SETTEXT message to the status bar:

SendMessage(Form1.StatusBar1.Handle, WM_SETTEXT, 0, LongInt(PChar(msg)));

SendMessage will send a message directly to a control and bypass the window 
procedure of the form that owns it.

Why did I go to all this trouble? For the very same reason that I used EnableWindow 
for the button that creates the thread. But unfortunately, unlike the single call 
to EnableWindow, there's no other way to set the text of a control other than 
sending it the WM_SETTEXT message.

The point to all this sneaking behind the VCL is that for the most part, it's not 
safe to access VCL properties or procedures in threads. In fact, the objects that 
are particularly dangerous to access from threads are those descended from 
TComponent. These comprise a large part of the VCL, so in cases where you have to 
perform some interaction with them from a thread, you'll have to use a roundabout 
method. But as you can see from the code above, it's not all that difficult.

Of the thousands of functions in the Windows API, CreateThread is one of the most simple and straightforward. I spent a lot of time explaining things here, but there's a lot of ground I didn't cover. Use this example as a template for your thread exploration. Once you get the hang of it, you'll use threads in practically everything you do.

			
Vote: How useful do you find this Article/Tip?
Bad Excellent
1 2 3 4 5 6 7 8 9 10

 

Advertisement
Share this page
Advertisement
Download from Google

Copyright © Mendozi Enterprises LLC