Author: Lou Adler
I have several programs that run large queries, and would like to move the query
processing to one or more background threads. How can I implement this in my
program?
Answer:
NOTE: It's tough to discuss this subject without providing some background
information. Threaded programming is so new to many programmers, I'd be remiss in
my duties as The Delphi Pro if I didn't cover the most fundamental subjects of
threaded programming. If you already have a good idea of the fundamentals of
creating and running threads with the TThread component, you can skip to the
section that deals with running queries in threadsBOOK1. -- The Delphi Pro
Process and Thread Basics
Many programmers, especially those new to programming with multiple threads,
believe threads are the sole domain of programming gurus. But it's all a matter of
understanding some fundamental concepts about how threads work in Delphi and how
you can apply threads in your code.
If what you're programming has to do with the User Interface, you don't want to use
multiple threads because threads require resources. In fact, every time you create
a thread, it gets the same size stack as the main thread of a program (we'll
discuss this below). If you have a lot of threads, you'll take up a lot of
resources. So the idea is to be judicious in your use of threads. It may be
tempting to create a bunch of threads to handle a bunch of different tasks because
creating threads, as you will see, is fairly easy. But you don't want to create
threads just for the sake of creating threads. In addition to taking up resources,
every executing thread creates another time slice on the CPU, forcing it to handle
more tasks. The net result is that the computer will slow way down. But there are
some instances in which running background threads makes a lot of sense:
It's ideal to create background threads when:
your program will execute a long process like a huge query or complex calculation
that will take several seconds or minute to execute. In single-threaded programs in
Win16 and Win32 alike, the interface becomes totally unresponsive if you execute a
long process. Creating a separate thread of execution to run in the background
frees up the user interface.
your program runs in a multi-processor system. Windows NT, for example, has the
ability to work SMP systems. With multi-threaded programs, individual threads may
be executed on different processors, which means you take full advantage of
balancing processor load.
your program will need to execute a few processes at a time. One caveat: If you're
running on a single CPU system and if the processes your program will be executing
are pretty complex and will require lots of CPU cycles, it doesn't make sense to
have several simultaneous threads running. However, I've found that with smaller
operations that can execute in a few seconds, it's a pretty nice feature to have.
In Windows 95 and NT (I'll refer to both systems as Win32 throughout this article),
every program loaded into memory is called a process. Many people new to threads
(including myself) make the mistake of believing that the definition of a process
is interchangeable with that of a thread. It's not.
Processes are their own entities. Though the name "processes" implies a form of
activity, a process does nothing. It is merely a memory address placeholder for its
threads and a space where executable code gets loaded.
A process' thread is what actually does the work in a program. When a process is
created in Win32, it automatically has an execution thread associated with it. This
is called the main thread of the program. Other threads can be instantiated within
a process, but you won't see many programs using multiple threads of execution. A
thread can only be associated with one process, but a process can have many
threads. Therefore, there is a distinct one-way, one-to-many relationship between
processes and threads.
The TThread Object
Traditionally, processes (executing programs) are created in Win32 using the WinAPI
CreateProcess and threads are created using CreateThread. In fact, many advanced
Delphi and Windows programmers I've spoken with say that using the WinAPI call is
their method of preference. With Delphi 2.0, the Delphi team created a wrapper
class called TThread that encapsulates the WinAPI thread calls. TThread provides
developers with a programmatic interface for creating multiple threads of execution
in their programs. It also makes the job of creating and maintaining threads easier
than directly using WinAPI calls.
Does this come at a price? I don't know. I have several multithreaded applications
using both straight WinAPI calls and the TThread object and haven't noticed any
significant differences. But my test arena was not as wide as it should have been
to accurately gauge performance differences.
Most of the VCL is not thread-safe -- it's very important to take this into
consideration when creating multiple threads of execution. If you call a VCL object
from within a thread, most likely you'll raise an exception, because many of the
VCL objects were not written with any type of synchronization code to ensure data
integrity when called at random times from anything but the main thread.
Essentially, they can only receive messages from a single thread. If they get a
message from another thread, they'll hiccup, and your program will probably crash.
Fortunately, TThread has a very simple way of safely making calls into the VCL that
we'll discuss in a bit.
Let's look at the TThread's structure.
Here's the declaration for the TThread object:
1 TThread = class
2 private
3 FHandle: THandle;
4 FThreadID: THandle;
5 FTerminated: Boolean;
6 FSuspended: Boolean;
7 FMainThreadWaiting: Boolean;
8 FFreeOnTerminate: Boolean;
9 FFinished: Boolean;
10 FReturnValue: Integer;
11 FOnTerminate: TNotifyEvent;
12 FMethod: TThreadMethod;
13 FSynchronizeException: TObject;
14 procedure CallOnTerminate;
15 function GetPriority: TThreadPriority;
16 procedure SetPriority(Value: TThreadPriority);
17 procedure SetSuspended(Value: Boolean);
18 protected
19 procedure DoTerminate; virtual;
20 procedure Execute; virtual; abstract;
21 procedure Synchronize(Method: TThreadMethod);
22 property ReturnValue: Integer read FReturnValue write FReturnValue;
23 property Terminated: Boolean read FTerminated;
24 public
25 constructor Create(CreateSuspended: Boolean);
26 destructor Destroy; override;
27 procedure Resume;
28 procedure Suspend;
29 procedure Terminate;
30 function WaitFor: Integer;
31 property FreeOnTerminate: Boolean read FFreeOnTerminate write FFreeOnTerminate;
32 property Handle: THandle read FHandle;
33 property Priority: TThreadPriority read GetPriority write SetPriority;
34 property Suspended: Boolean read FSuspended write SetSuspended;
35 property ThreadID: THandle read FThreadID;
36 property OnTerminate: TNotifyEvent read FOnTerminate write FOnTerminate;
37 end;
Its structure is quite simple -- and simple is good
In most components there are only a few procedures and properties you need to think
about; this is not an exception with TThread. The only methods you'll need to worry
about are Execute, Create, and Synchronize; and the only property that you'll
usually need to access is FreeOnTerminate.
Key Methods and Properties of TThread
The key methods and property of TThread are listed below in Table 1.
Table 1 -- Key Attributes of TThread
Attribute: Create Parameters: CreateSuspended: Boolean
The Create method allocates memory, starts the thread and specifies the thread
function in CreateThread as the Execute procedure. Here, as in any Create method,
you can initialize vars and perform some preliminary operations. However, unlike a
normal Create method, the thread is already executing by the time the method ends.
Usually this isn't a problem because you'll just create the object and forget about
it. But if you have to do any processing before the thread starts executing, set
the CreateSuspended parameter to False. This allocates memory for the thread and
sets it up, but the thread will not execute until you make a call to the Resume
procedure. This is useful, especially if you need to set up values your thread will
need over the course of its lifetime.
Attribute: Execute Parameters: None
Execute is your TThread's central execution method. It's fairly synonymous with a
main processing or central control procedure or function you might use to execute
all the procedures in a particular unit. The only difference is that with a TThread
object, the first method that is called must be called Execute. This doesn't mean
that you can't call another procedure which acts in that capacity from Execute.
It's definitely more convenient to put your execution code here because that's
Execute's purpose.
One important note: If you look in the object declaration of TThread, you'll see
that it is declared as a virtual; abstract; method. This means it's not implemented
in any way, shape or form; therefore, it's up to you to provide the code. And
there's no inherited functionality, so you'll never make a call to inherited
Execute; in your own implementation.
Attribute: Synchronize Parameters: Method:TThreadMethod
Synchronize is your key to safely accessing the VCL in your thread. When
Synchronize executes, your thread becomes a part of the main thread of the program,
and in the process, suspends the operation of the main thread. This means the VCL
can't receive messages from other threads other than the one you're synchronizing
to the main thread, which in turn makes it safe to execute any VCL calls.
Think of Synchronize as a middleman in a transaction. You have a buyer, which is
the main thread of the program, and a seller of services, which is another thread
of execution created within the same process. The seller would like to sell the
buyer some goods -- in our case, do some operation on the VCL components running in
the main thread -- but the buyer doesn't really know the seller, and is appalled at
how the seller actually performs his service, so is afraid the seller's service may
have a negative effect on him. So the seller enlists the help of an agent (the
Synchronize procedure) to smooth things out, take the buyer out to lunch so the
seller can do his thing.
The seller is a procedure that performs the action on behalf of the thread. It
doesn't have to be a specific type, but it must be a method of the thread. Say I
have a long process running in a background thread that at certain times must
update the text in a TPanel on the main form to give feedback about the thread's
current status to the user. If I wrote in my code Form1.Panel1.Caption := 'This is
my update message', I'd raise an exception -- most likely an access violation
error. But if I encapsulate the call in a procedure, then call Synchronize, the
message will be sent and text will be updated. Synchronize acted as a middleman so
my status message could be passed to the TPanel.
I've may have confused rather than enlightened you! Just remember this:
When you want to call VCL objects from another thread, create a wrapper procedure
for the operations you want to carry out, then use Synchronize to synchronize your
thread with the main thread so your call can be safely executed.
Synchronize is meant to be used really quickly. Execute it, then get out of it as
soon as you can. While Synchronize is running, you essentially have only one thread
working. The main thread is suspended.
Attribute: FreeOnTerminate Parameters: Set property to Boolean value
This is a really useful property. Typically, once your execute procedure finishes,
the Terminated Boolean property is set to False. However, the thread still exists.
It's not running, but it's taking up space, and it's up to you to free the thread.
But by setting this property in the Create constructor to True, the thread is
immediately freed -- and with it, all the resources it took up -- after it's
finished executing.
A Real-life Example
Most people get more out of seeing code examples to implement a certain concept.
Below are code excerpts from a program that I wrote that performs a bunch of
queries in sequence. First we have the type declaration:
38 type
39 TEpiInfoProc = class(TThread)
40 {Base processing class for Episode Information processing}
41 private
42 FStatMsg: string;
43 FSession: TSession;
44 tblPrimary,
45 tblHistory,
46 tblSymmetry: string;
47 FIter: Integer;
48 property Iteration: Integer read FIter write FIter;
49 procedure eiCreateEpiTemp1; //Performs initial joins
50 procedure eiCreateEpiTemp2; //Creates new table of summarized info
51 procedure eiGetClassifications(clState, //'AMI', 'Asthma', etc.
52 clName, //'H1', 'H2', etc.
53 priFld, //Join field from Primary
54 hstFld: string; //Join field from History
55 bMode: TBatchMode); //Batch Mode (will always be
56 //batCopy 1st time);
57 procedure eiCreateEpiInfoTable(clSrc, clDst, clName: string);
58 procedure eiCreateHistory(HistIndicator: string);
59
60 //Generic processing methods
61 procedure EnableBtns;
62 procedure GetTableNames;
63 procedure UpdStatus;
64 procedure IndexTbl(dbName, tblName, idxName, fldName: string; idxOpts:
65 TIndexOptions);
66 protected
67 procedure Execute; override;
68 public
69 constructor Create;
70 end;
The above is like anything you see in Delphi when you declare a descendant of a
class. You declare your variables, properties, and methods just like anything else.
Here's the Create constructor for the thread:
71 constructor TEpiInfoProc.Create;
72 begin
73 inherited Create(True);
74 FSession := TSession.Create(Application);
75 with FSession do
76 begin
77 SessionName := 'EpiInfoSession';
78 NetFileDir := Session.NetFileDir;
79 PrivateDir := 'D:\EpiPriv';
80 end;
81 FreeOnTerminate := True;
82 Resume;
83 end;
Notice I'm creating a new TSession instance. When we discuss running queries in
threads, I'll go into more depth about this. At this point the important thing to
note is that I create the thread in a suspended state by calling the inherited
Create and setting its CreateSuspended parameter to True. This allows me to perform
the code immediately below the inherited Create before the thread code actually
starts running. Now, on to the Execute procedure.
84 procedure TEpiInfoProc.Execute;
85 var
86 N, M: Integer;
87 begin
88 try
89 Synchronize(EnableBtns);
90 //Set enabled property of buttons to opposite of current state
91 Synchronize(GetTableNames); //Get Names
92 FStatMsg := 'Now performing initial summarizations';
93 Synchronize(UpdStatus);
94 ShowMessage('Did it!');
95 Exit;
96 eiCreateEpiTemp1;
97 eiCreateEpiTemp;
98 {Create H1 - H9 tables}
99 for N := 0 to 8 do
100 for M := 0 to 5 do
101 begin
102 FStatMsg := 'Now performing ' + arPri[M] + ' field extract for ' + arDis[N];
103 Synchronize(UpdStatus);
104 Iteration := M;
105 //first iteration must be a batCopy, then batAppend thereafter
106 if (M = 0) then
107 eiGetClassifications(arDis[N], arCls[N], arPri[M], arHst[M], batCopy)
108 else
109 eiGetClassifications(arDis[N], arCls[N], arPri[M], arHst[M], batAppend);
110 end;
111
112 {Now do Outer Joins}
113 for N := 0 to 8 do
114 begin
115 FStatMsg := 'Now creating ' + arDst[N];
116 Synchronize(UpdStatus);
117 eiCreateEpiInfoTable(arSrc[N], arDst[N], arCls[N]);
118 end;
119
120 IndexTbl('EPIINFO', 'EpiInfo', 'Primary', 'Episode', [ixPrimary]);
121
122 for N := 0 to 8 do
123 eiCreateHistory(arCls[N]);
124
125 FStatMsg := 'Operation Complete!';
126 Synchronize(UpdStatus);
127 Synchronize(EnableBtns);
128 except
129 Terminate;
130 Abort;
131 end;
132 end;
Notice all my calls to Synchronize, especially the synchronized call to UpdStatus.
Immediately preceding this call, I set a variable called FStatMsg to some text.
UpdStatus uses this text to set the SimpleText of a TStatusBar on the main form of
the program.
Why not just pass this as a string variable parameter to UpdStatus? Because
synchronized methods cannot have parameters. If you need to pass parameters, you
must either set them in variables or create a structure to hold values your
synchronized method can then access and pass to your main form.
Also take note of the looping structures I've written. You might be tempted to run
a loop within a synchronized method. Although your code will work, this defeats the
purpose of multiple threads because of what we discussed above. Synchronization
makes your thread part of the main thread during the time it is executing, meaning
you have only one thread actually running. This reduces your program to a single
thread of execution. And if you have a potentially long process like several
sequential queries executed from within a loop like I have above, forget it! Your
program will act just like a single-threaded application until you're out the loop.
Running Queries in Threads
In order to successfully run queries in threads, you must follow a few cardinal
rules:
For each thread you create that will be executing queries, you must have a separate
instance of a TSession created along with the thread. (See Create constructorBOOK2
above)
Simply put, TSessions essentially define the multi-threadness of the BDE. The BDE
requires separate TSessions to allow multiple access to shared resources. Failing
to do this will cause some very strange things to occur in your program (it will
crash with an BDE exception of some sort.)
Every data-access VCL component you instantiate within a thread must have its
SessionName property set to that of the SessionName of the TSession created with
your thread.
If you don't do this, the data-access component will default to the TSession
created along with the main thread; this could have serious ramifications. If you
assign components to a new TSession and forget to do so for others, when the
components interact with each other with let's say a TBatchMove moving data from a
query to a TTable, you'll get an error that says you're trying to perform an
operation on objects in different sessions. Not good. So just to ensure that
everything is safe, keep the data-access components you create in separate threads
distinct by assigning the SessionNames to the SessionName of the TSession you
create along with your thread.
You must create synchronized methods for any operation that will be accessing
data-access components embedded on a form.
I can't stress enough the importance of performing any call that will access VCL
components within a Synchronize call. You're looking for trouble if you don't.
Let's say you run a background query that will provide information in a visual form
in a TDBGrid. Running the query is the easy part. But once you're finished, you
have to have a way of manipulating a TDataSource so that its DataSet property will
point to the query you've run in the thread. Setting this must be done in a call to
a synchronized method that does the setting for you.
Building on the previous rule, queries that will be providing data for data-aware
components such as a TDBGrid must be persistent.
Once you free a thread, its resources are freed too. If you create a TQuery within
the context of a TThread, your query is gone once the thread terminates and frees.
The easiest thing to do then is to drop a TQuery on a form and run it from within
the thread. That way, its handle remains valid even though the thread has finished
its work.
Of the above rules, 1 and 2 are the most important elements for successfully
running queries in separate threads of execution. While rules 3 and 4 are
important, under certain conditions your queries will not run if you don't obey
rules 1 and 2.
"Visual" and Non-visual Background Queries
Now that we've established ground rules regarding running queries in threads, we
must take into consideration a couple of implementation paths. I put the visual in
quotes above to denote queries whose results will be displayed in a data-aware
component of some sort. Non-visual background queries don't provide any visual
result. They just run, write to persistent data stores and return. How you
implement these methods is significantly different -- we'll discuss them in
separate sections below.
In either of these cases, the aim is to free up the user interface. One of my
biggest frustrations with writing programs that process a lot of information is
that once I've started execution, I can't do anything with the interface. I can't
minimize or maximize the application; I can't move the window. Ever since I've
learned to implement threaded technology in my programs, I need not worry about
that. It's a real boon to my productivity.
Running Totally Background Queries
This method is a challenge; you have to code everything yourself. It's not a lot of
code, but there are certain things to consider that you can take for granted when
you drop VCL components onto a form. This method is very useful for "Data Mining"
types of programs, in which you're querying huge tables and coming up with highly
refined, summarized result sets. The operations that typify this type of
application are several queries performed in succession. Were you to run this kind
of application in a single-threaded program, you can effectively kiss your
productivity goodbye because your user interface will be locked.
The example I'll be using for this discussion is the exampleBOOK3 I used aboveBOOK3
because that application is a data mining type of application. It was built to run
several sequential queries against Paradox tables with hundreds of thousands of
records (pretty big for Paradox tables).
Let's revisit the type declaration of thread:
133 type
134 TEpiInfoProc = class(TThread)
135 {Base processing class for Episode Information processing}
136 private
137 FStatMsg: string;
138 FSession: TSession;
139 tblPrimary,
140 tblHistory,
141 tblSymmetry: string;
142 FIter: Integer;
143 property Iteration: Integer read FIter write FIter;
144 procedure eiCreateEpiTemp1; //Performs initial joins
145 procedure eiCreateEpiTemp2; //Creates new table of summarized info
146 procedure eiGetClassifications(clState, //'AMI', 'Asthma', etc.
147 clName, //'H1', 'H2', etc.
148 priFld, //Join field from Primary
149 hstFld: string; //Join field from History
150 bMode: TBatchMode); //Batch Mode (will always be
151 //batCopy 1st time);
152 procedure eiCreateEpiInfoTable(clSrc, clDst, clName: string);
153 procedure eiCreateHistory(HistIndicator: string);
154
155 //Generic processing methods
156 procedure EnableBtns;
157 procedure GetTableNames;
158 procedure UpdStatus;
159 procedure IndexTbl(dbName, tblName, idxName, fldName: string; idxOpts:
160 TIndexOptions);
161 protected
162 procedure Execute; override;
163 public
164 constructor Create;
165 end;
There's something in the type declaration that I didn't bring up previously. Notice
the second private variable FSession. Then, look at the constructor Create code
below:
166 constructor TEpiInfoProc.Create;
167 begin
168 inherited Create(True);
169 FSession := TSession.Create(Application);
170 with FSession do
171 begin
172 SessionName := 'EpiInfoSession';
173 NetFileDir := Session.NetFileDir;
174 PrivateDir := 'D:\EpiPriv';
175 end;
176 FreeOnTerminate := True;
177 Resume;
178 end;
I simply instantiate a new session in my Create constructor for my queries and
tables to point to during the course of execution. If you don't have the latest
update to Delphi and you look in the help file under TSession, you're warned not to
create a TSession on the fly. Why? Truthfully, I don't know. I broke the rules
anyway when I originally saw this warning because after looking at the VCL source
for the TSession object in the DB.PAS file, I didn't see anything in the TSession
object that would lead me to believe that I couldn't instantiate a TSession object
on the fly. This changed in 2.01 -- the help file makes no such warning -- so it's
not an issue. Of all the things that I'm doing in the thread, creating this
TSession is the absolute key operation because it provides a common ground for all
the data access components that I instantiate in the thread.
During the course of executionBOOK4, I make calls to several procedures that
perform queries on several different tables. However, they all operate similarly,
so it's only necessary to list one of the procedures to illustrate how you should
do your queries in a thread. Here's an example procedure:
179 procedure TEpiInfoProc.eiCreateEpiTemp1;
180 var
181 sqlEI: TEnhQuery;
182 begin
183
184 sqlEI := TEnhQuery.Create(Application);
185 with sqlEI do
186 begin
187 SessionName := FSession.SessionName;
188 DatabaseName := 'Primary';
189 DestDatabaseName := 'PRIVATE';
190 DestinationTable := 'epitemp1.db';
191 DestBatchMoveMode := batCopy;
192 DoBatchMove := True;
193 with SQL do
194 begin
195 Clear;
196 Add('SELECT DISTINCT d.Episode, d.PatientID, d.Paid AS TotPd, d.Charge AS
197 TotChg, D1.Start'
198 Add('FROM "' + tblPrimary + '" d LEFT OUTER JOIN "' + tblSymmetry + '" D1 ');
199 Add('ON (D1.Episode = d.Episode)');
200 Add('ORDER BY d.Episode, d.PatientID, d.Paid, d.Charge, D1.Start');
201 end;
202 try
203 try
204 Execute;
205 except
206 raise;
207 Abort;
208 end;
209 finally
210 Free;
211 end;
212 end;
213 end;
The important thing to notice in the code example above is that the first property
I set for the query I create is its SessionName property. This falls in line with
obeying rulesBOOK1 1 and 2 that I mentioned above. I did this first so I'd be in
compliance with them right away, though you can set properties in any order. The
whole point to this is that looking at the procedure, it's no different from any
type of procedure you'd use to create a query and execute it in code. The only
difference is that you don't rely on the default TSession; you use the one you
created at construction time.
While I can't release what the application above actually does, I'll give you some
gory details. On the average, the application requires three to four hours to
execute completely, based on the size of the client database. Most of the client
databases are several hundred megabytes in size, and all are in excess of 600MB per
table, so you can imagine that with the enormity of the data sets, a single
threaded application would just sit there and appear to be locked. But running the
queries in the background frees the interface, so status messages can be supplied
quite easily, and the form remains active during the run.
I took an easy way out in this program to prevent multiple threads from executing.
Typically what you'll do is use a system mutex or a semaphore to provide a
mechanism signaling that you have a process running. But I found it much easier to
just disable all the buttons on the form, so the user could still move the form
around and even enter selection criteria but couldn't execute the new process until
the current process has finished. It took much less code to implement a disabling
feature than to implement a system mutex.
Running Background Queries that will Produce Visible Result Sets
You won't need to do this often because typically, queries run to display data in a
grid or some other data-aware component are usually run against smaller tables and
execute fairly quickly, so you can get away with running a single thread of
execution. There are some instances in which you might need to query a couple of
unrelated tables at the same time, so running the queries in separate threads of
execution makes a lot of sense.
Below is unit code from a test unit I wrote to illustrate this purpose. On my form
I've dropped two of each of the following components: TQuery, TSession, TDataSource
and a TDBGrid. I also have a button that will perform the thread creation. Let's
look at the code, then discuss it:
214 unit thrtest;
215
216 interface
217
218 uses
219 Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, DB,
220 DBTables,
221 Grids, DBGrids, StdCtrls;
222
223 type
224 TForm1 = class(TForm)
225 DataSource1: TDataSource;
226 Query1: TQuery;
227 DBGrid1: TDBGrid;
228 Button1: TButton;
229 DataSource2: TDataSource;
230 Query2: TQuery;
231 DBGrid2: TDBGrid;
232 Session1: TSession;
233 Session2: TSession;
234 procedure Button1Click(Sender: TObject);
235 end;
236
237 //Thread class declaration - very simple
238 TQThread = class(TThread)
239 private
240 FQuery: TQuery;
241 protected
242 procedure Execute; override;
243 public
244 constructor Create(Query: TQuery);
245 end;
246
247 var
248 Form1: TForm1;
249
250 implementation
251
252 {$R *.DFM}
253
254 constructor TQThread.Create(Query: TQuery);
255 begin
256 inherited Create(True); // Create thread in a suspendend state so we can prepare
257 vars
258 FQuery := Query; //Set up local query var to be executed.
259 FreeOnTerminate := True; //Free thread when finished executing
260 Resume;
261 end;
262
263 procedure TQThread.Execute;
264 begin
265 FQuery.Open; //Perform the query
266 end;
267
268 procedure TForm1.Button1Click(Sender: TObject);
269 begin
270 TQThread.Create(Query1);
271 TQThread.Create(Query2);
272 end;
273
274 end.
I've made the thread intentionally easy. The only thing you pass to the constructor
is a query. The rest is all taken care of.
Before I did any coding of the above, I did the following with the components on
the form:
DataSource1 and DataSource2 have their DataSet properties set to Query1 and Query2,
respectively. Likewise, DBGrid1 and DBGrid2 have their DataSource properties set to
DataSource1 and DataSource2, respectively.
Query1 and Query2 have their SessionName property set to the SessionName property
of Session1 and Session2, respectively.
Both TQuerys have their DatabaseName properties set to DBDEMOS. Query1's SQL is
simple : 'SELECT * FROM "employee.db".' Query2's SQL is simple as well: 'SELECT *
FROM "customer.db".'
This is a really simplistic example that works amazingly well. When the user
presses the button, the program creates two new threads of type TQThread. Since we
pass in the query we want to execute, the thread knows which one to operate on.
Notice that I didn't put any synchronization code in this example. Some might think
that you have to do this for a DataSource component, but the DataSource is
Session-less, so it's impossible. Besides, the DataSource is merely a conduit
between the Query and the DBGrid. It's fairly inert.
As in any program, you can make this much more complex. For example, you can set
the DatabaseName and SQL of the TQuerys at runtime, making this a really flexible
display tool. You can add a batchmove facility at the tail-end of the run so that
in addition to displaying the results in a DBGrid, the program will also write to
result tables. Play around with this to see how you can expand on the concept.
Conclusion
Multi-threading is a breakthrough and revolutionary programming technology for many programmers. Note, that on Unix systems, multithreading has existed for several years. But for Windows platforms, this hasn't been the case until a few years ago. Also, I didn't cover some of the really advanced stuff that you can do with threads. For that, you'll have to refer to advanced-level Win32 development books. But hopefully, in the course of this discussion, you've reached a level of understanding that will get you on your way to constructing your own multi-threaded applications.
|