The VCL provides several objects that make writing multi-threaded applications easier. Multi-threaded applications are applications that include several simultaneous paths of execution. While using multiple threads requires careful thought, it can enhance your programs by
Note: Not all operating systems implement true multi-processing, even when it is supported by the underlying hardware. For example Windows 95 only simulates multiprocessing, even if the underlying hardware supports it.
For most applications, you can use a thread object to represent an execution thread in your application. Thread objects simplify writing multi-threaded applications by encapsulating the most commonly needed uses of threads.
Note: Thread objects do not allow you to control the security attributes or stack size of your threads. If you need to control these, you must use the BeginThread function. Even when using BeginThread, you can still benefit from some of the thread synchronization objects and methods described in "Coordinating threads". For more information on using BeginThread, see the online help.
To use a thread object in your application, you must create a new descendant of TThread. To create a descendant of TThread, choose File|New from the main menu. In the new objects dialog box, select Thread Object. You are prompted to provide a class name for your new thread object. After you provide the name, Delphi creates a new unit file to implement the thread.
Note: Unlike most dialog boxes in the IDE that require a class name, the New Thread Object dialog does not automatically prepend a 'T' to the front of the class name you provide.
The automatically generated file contains the skeleton code for your new thread object. If you named your thread TMyThread, it would look like the following:
unit Unit2;
interface
uses
Classes;
type
TMyThread = class(TThread)
private
{ Private declarations }
protected
procedure Execute; override;
end;
implementation
{ TMyThread }
procedure TMyThread.Execute;
begin
{ Place thread code here }
end;
end.
You must fill in the code for the Execute method. These steps are described in the following sections.
If you want to write initialization code for your new thread class, you must override the Create method. Add a new constructor to the declaration of your thread class and write the initialization code as its implementation. This is where you can assign a default priority for your thread and indicate whether it should be freed automatically when it finishes executing.
Priority indicates how much preference the thread gets when the operating system schedules CPU time among all the threads in your application. Use a high priority thread to handle time critical tasks, and a low priority thread to perform other tasks. To indicate the priority of your thread object, set the Priority property. Priority values fall along a seven point scale, as described in Table 8.1:
The thread executes only when the system is idle. Windows won't interrupt other threads to execute a thread with tpIdle priority. | |
Warning: Boosting the thread priority of a CPU intensive operation may "starve" other threads in the application. Only apply priority boosts to threads that spend most of their time waiting for external events.
The following code shows the constructor of a low-priority thread that performs background tasks which should not interfere with the rest of the application's performance:
constructor TMyThread.Create(CreateSuspended: Boolean);
{
inherited Create(CreateSuspended);
Priority := tpIdle;
}
Usually, when threads finish their operation, they can simply be freed. In this case, it is easiest to let the thread object free itself. To do this, set the FreeOnTerminate property to True.
There are times, however, when the termination of a thread must be coordinated with other threads. For example, you may be waiting for one thread to return a value before performing an action in another thread. To do this, you do not want to free the first thread until the second has received the return value. You can handle this situation by setting FreeOnTerminate to False and then explicitly freeing the first thread from the second.
The Execute method is your thread function. You can think of it as a program that is launched by your application, except that it shares the same process space. Writing the thread function is a little trickier than writing a separate program because you must make sure that you don't overwrite memory that is used by other threads in your application. On the other hand, because the thread shares the same process space with other threads, you can use the shared memory to communicate between threads.
When you use objects from the VCL object hierarchy, their properties and methods are not guaranteed to be thread-safe. That is, accessing properties or executing methods may perform some actions that use memory which is not protected from the actions of other threads. Because of this, a main VCL thread is set aside for access of VCL objects. This is the thread that handles all Windows messages received by components in your application.
If all objects access their properties and execute their methods within this single thread, you need not worry about your objects interfering with each other. To use the main VCL thread, create a separate routine that performs the required actions. Call this separate routine from within your thread's Synchronize method. For example:
procedure TMyThread.PushTheButton; begin Button1.Click; end; ... procedure TMyThread.Execute; begin ... Synchronize(PushTheButton); ... end;
Synchronize waits for the main VCL thread to enter the message loop and then executes the passed method.
Note: Because Synchronize uses the message loop, it does not work in console applications. You must use other mechanisms, such as critical sections, to protect access to VCL objects in console applications.
You do not always need to use the main VCL thread. Some objects are thread-aware. Omitting the use of the Synchronize method when you know an object's methods are thread-safe will improve performance because you don't need to wait for the VCL thread to enter its message loop. You do not need to use the Synchronize method in the following situations:
When using data access components, you must still wrap all calls that involve data-aware controls in the Synchronize method. Thus, for example, you need to synchronize calls that link a data control to a dataset by setting the DataSet property of the data source object, but you don't need to synchronize to access the data in a field of the dataset.
For more information about using database sessions with threads, see "Managing multiple sessions".
Your Execute method and any of the routines it calls have their own local variables, just like any other Object Pascal routines. These routines also can access any global variables. In fact, global variables provide a powerful mechanism for communicating between threads.
Sometimes, however, you may want to use variables that are global to all the routines running in your thread, but not shared with other instances of the same thread class. You can do this by declaring thread-local variables. Make a variable thread-local by declaring it in a threadvar section. For example,
threadvar
x : integer;
declares an integer type variable that is private to each thread in the application, but global within each thread.
The threadvar section can only be used for global variables. Pointer and Function variables can't be thread variables. Types that use copy-on-write semantics, such as long strings don't work as thread variables either.
Your thread begins running when the Execute method is called (see "Executing thread objects") and continues until Execute finishes. This reflects the model that the thread performs a specific task, and then stops when it is finished. Sometimes, however, an application needs a thread to execute until some external criterion is satisfied.
You can allow other threads to signal that it is time for your thread to finish executing by checking the Terminated property. When another thread tries to terminate your thread, it calls the Terminate method. Terminate sets your thread's Terminated property to True. It is up to your Execute method to implement the Terminate method by checking and responding to the Terminated property. The following example shows one way to do this:
procedure TMyThread.Execute;
begin
while not Terminated do
PerformSomeTask;
end;
You can centralize the code that cleans up when your thread finishes executing. Just before a thread shuts down, an OnTerminate event occurs. Put any clean-up code in the OnTerminate event handler to ensure that it is always executed, no matter what execution path the Execute method follows.
The OnTerminate event handler is not run as part of your thread. Instead, it is run in the context of the main VCL thread of your application. This has two implications:
For more information about the main VCL thread, see "Using the main VCL thread".
When writing the code that runs when your thread is executed, you must consider the behavior of other threads that may be executing simultaneously. In particular, care must be taken to avoid two threads trying to use the same global object or variable at the same time. In addition, the code in one thread can depend on the results of tasks performed by other threads.
To avoid clashing with other threads when accessing global objects or variables, you may need to block the execution of other threads until your thread code has finished an operation. Be careful not to block other execution threads unnecessarily. Doing so can cause performance to degrade seriously and negate most of the advantages of using multiple threads.
Some objects have built-in locking that prevents the execution of other threads from using that object instance.
For example, canvas objects (TCanvas and descendants) have a Lock method that prevents other threads from accessing the canvas until the Unlock method is called.
The VCL also includes a thread-safe list object, TThreadList. Calling TThreadList.LockList returns the list object while also blocking other execution threads from using the list until the UnlockList method is called. Calls to TCanvas.Lock or TThreadList.LockList can be safely nested. The lock is not released until the last locking call is matched with a corresponding unlock call in the same thread.
If objects do not provide built-in locking, you can use a critical section. Critical sections work like gates that allow only a single thread to enter at a time. To use a critical section, create a global instance of TCriticalSection. TCriticalSection has two methods, Acquire (which blocks other threads from executing the section) and Release (which removes the block).
Each critical section is associated with the global memory you want to protect. Every thread that accesses that global memory should first use the Acquire method to ensure that no other thread is using it. When finished, threads call the Release method so that other threads can access the global memory by calling Acquire.
Warning: Critical sections only work if every thread uses them to access the associated global memory. Threads that ignore the critical section and access the global memory without calling Acquire can introduce problems of simultaneous access.
For example, consider an application that has a global critical section variable, LockXY, that blocks access to global variables X and Y. Any thread that uses X or Y must surround that use with calls to the critical section such as the following:
LockXY.Acquire; { lock out other threads }
try
Y := sin(X);
finally
LockXY.Release;
end;
When you use critical sections to protect global memory, only one thread can use the memory at a time. This can be more protection than you need, especially if you have an object or variable that must be read often but to which you very seldom write. There is no danger in multiple threads reading the same memory simultaneously, as long as no thread is writing to it.
When you have some global memory that is read often, but to which threads occasionally write, you can protect it using TMultiReadExclusiveWriteSynchronizer. This object acts like a critical section, but one which allows multiple threads to read the memory it protects as long as no thread is writing to it. Threads must have exclusive access to write to memory protected by TMultiReadExclusiveWriteSynchronizer.
To use a multi-read exclusive-write synchronizer, create a global instance of TMultiReadExclusiveWriteSynchronizer that is associated with the global memory you want to protect. Every thread that reads from this memory must first call the BeginRead method. BeginRead ensures that no other thread is currently writing to the memory. When a thread finishes reading the protected memory, it calls the EndRead method. Any thread that writes to the protected memory must call BeginWrite first. BeginWrite ensures that no other thread is currently reading or writing to the memory. When a thread finishes writing to the protected memory, it calls the EndWrite method, so that threads waiting to read the memory can begin.
Warning: Like critical sections, the multi-read exclusive-write synchronizer only works if every thread uses it to access the associated global memory. Threads that ignore the synchronizer and access the global memory without calling BeginRead or BeginWrite introduce problems of simultaneous access.
When using objects in the VCL, use the main VCL thread to execute your code. Using the main VCL thread ensures that the object does not indirectly access any memory that is also used by VCL objects in other threads. See "Using the main VCL thread" for more information on the main VCL thread.
If the global memory does not need to be shared by multiple threads, consider using thread-local variables instead of global variables. By using thread-local variables, your thread does not need to wait for or lock out any other threads. See "Using thread-local variables" for more information about thread-local variables.
If your thread must wait for another thread to finish some task, you can tell your thread to temporarily suspend execution. You can either wait for another thread to completely finish executing, or you can wait for another thread to signal that it has completed a task.
To wait for another thread to finish executing, use the WaitFor method of that other thread. WaitFor doesn't return until the other thread terminates, either by finishing its own Execute method or by terminating due to an exception. For example, the following code waits until another thread fills a thread list object before accessing the objects in the list:
if ListFillingThread.WaitFor then
begin
with ThreadList1.LockList do
begin
for I := 0 to Count - 1 do
ProcessItem(Items[I]);
end;
ThreadList1.UnlockList;
end;
In the previous example, the list items were only accessed when the WaitFor method indicated that the list was successfully filled. This return value must be assigned by the Execute method of the thread that was waited for. However, because threads that call WaitFor want to know the result of thread execution, not code that calls Execute, the Execute method does not return any value. Instead, the Execute method sets the ReturnValue property. ReturnValue is then returned by the WaitFor method when it is called by other threads. Return values are integers. Your application determines their meaning.
Sometimes, you need to wait for a thread to finish some operation rather than waiting for a particular thread to complete execution. To do this, use an event object. Event objects (TEvent) should be created with global scope so that they can act like signals that are visible to all threads.
When a thread completes an operation that other threads depend on, it calls TEvent.SetEvent. SetEvent turns on the signal, so any other thread that checks will know that the operation has completed. To turn off the signal, use the ResetEvent method.
For example, consider a situation where you must wait for several threads to complete their execution rather than a single thread. Because you don't know which thread will finish last, you can't simply use the WaitFor method of one of the threads. Instead, you can have each thread increment a counter when it is finished, and have the last thread signal that they are all done by setting an event.
The following code shows the end of the OnTerminate event handler for all of the threads that must complete. CounterGuard is a global critical section object that prevents multiple threads from using the counter at the same time. Counter is a global variable that counts the number of threads that have completed.
procedure TDataModule.TaskThreadTerminate(Sender: TObject);
begin
...
CounterGuard.Acquire; { obtain a lock on the counter }
Dec(Counter); { decrement the global counter variable }
if Counter = 0 then
Event1.SetEvent; { signal if this is the last thread }
CounterGuard.Release; { release the lock on the counter }
...
end;
The main thread initializes the Counter variable, launches the task threads, and waits for the signal that they are all done by calling the WaitFor method. WaitFor waits for a specified time period for the signal to be set, and returns one of the values from Table 8.2
The following shows how the main thread launches the task threads and then resumes when they have all completed:
Event1.ResetEvent; { clear the event before launching the threads }
for i := 1 to Counter do
TaskThread.Create(False); { create and launch task threads }
if Event1.WaitFor(20000) != wrSignaled then
raise Exception;
{ now continue with the main thread. All task threads have finished }
Note: If you do not want to stop waiting for an event after a specified time period, pass the WaitFor method a parameter value of INFINITE. Be careful when using INFINITE, because your thread will hang if the anticipated signal is never received.
Once you have implemented a thread class by giving it an Execute method, you can use it in your application to launch the code in the Execute method. To use a thread, first create an instance of the thread class. You can create a thread instance that starts running immediately, or you can create your thread in a suspended state so that it only begins when you call the Resume method. To create a thread so that it starts up immediately, set the constructor's CreateSuspended parameter to False. For example, the following line creates a thread and starts its execution:
SecondProcess := TMyThread.Create(false); {create and run the thread }
Warning: Do not create too many threads in your application. The overhead in managing multiple threads can impact performance. The recommended limit is 16 threads per process on single processor systems. This limit assumes that most of those threads are waiting for external events. If all threads are active, you will want to use fewer.
You can create multiple instances of the same thread type to execute parallel code. For example, you can launch a new instance of a thread in response to some user action, allowing each thread to perform the expected response.
When the amount of CPU time the thread should receive is implicit in the thread's task, its priority is set in the constructor. This is described in "Initializing the thread". However, if the thread priority varies depending on when the thread is executed, create the thread in a suspended state, set the priority, and then start the thread running:
SecondProcess := TMyThread.Create(True); { create but don't run }
SecondProcess.Priority := tpLower; { set the priority lower than normal }
SecondProcess.Resume; { now run the thread }
A thread can be started and stopped any number of times before it finishes executing. To stop a thread temporarily, call its Suspend method. When it is safe for the thread to resume, call its Resume method. Suspend increases an internal counter, so you can nest calls to Suspend and Resume. The thread does not resume execution until all suspensions have been matched by a call to Resume.
You can request that a thread end execution prematurely by calling the Terminate method. Terminate sets the thread's Terminated property to True. If you have implemented the Execute method properly, it checks the Terminated property periodically, and stops execution when Terminated is True.
Distributed applications introduce additional challenges for writing multi-threaded applications. When considering how to coordinate threads, you must also keep in mind how other processes affect the threads in your application.
Usually, handling distributed threading issues is the responsibility of the server application. When writing servers, you must consider how requests from clients are serviced.
If each client request has its own thread, you must ensure that different client threads do not interfere with each other. In addition to the usual issues that arise when coordinating multiple threads, you may need to ensure that each client has a consistent view of your application. For example, you can't use thread variables to store information that must persist over multiple client requests if each time the client calls your application it uses a different thread. When clients change the values of object properties or global variables, they are influencing not only their own view of that object or variable, but the view of any other clients.
Message-based servers receive client request messages, perform some action in response to that message, and return messages to the client. Examples include internet server applications and simple services that you can write using sockets.
Usually, when writing message-based servers, each client message gets its own thread. When client messages are received, the application spawns a thread to handle the message. This thread runs until it sends a response to the client, and then terminates. You must be careful when using global objects and variables, but it is fairly easy to control how threads are created and run because client messages are all received and dispatched by the main application thread.
When writing servers for distributed objects, the threading issues are more complicated. Unlike message-based servers, where there is a point in the code where messages are received and dispatched, clients call into server objects by calling any of their methods or by accessing any of their properties. Because of this, there is no easy way for server applications to spawn separate threads for each client request.
When writing an .EXE that implements an object or objects for remote clients, client requests come in as threads. How this works depends on whether clients access your object using COM or CORBA.
When an Active Library implements the distributed object, threading is usually controlled by the technology (COM, DCOM, or MTS) that supports distributed object calls. When you first create your server library with the appropriate wizard, you are prompted to specify a threading model that dictates how client requests are assigned threads. These models include the following:
Note: Typically, a wizard assigns a threading model to your object. When you add multiple COM objects to an EXE, the application initializes COM with the highest level of thread support indicated (where single-threaded is the lowest and Both is highest). You can manually override the way your application initializes COM threading support by changing the global CoInitFlags variable in the program's main source file before the call to Application.Intitialize.
COM-based systems use the application's message loop to synchronize threads in all but the Multi-threaded apartment model (which is only available under DCOM). Because of this, you must ensure that any lengthy call made through a COM interface calls the application object's ProcessMessages method. Failure to do so prevents other clients from gaining access to your application, effectively making your library single-threaded.
When debugging multi-threaded applications, it can be confusing trying to keep track of the status of all the threads that are executing simultaneously, or even to determine which thread is executing when you stop at a breakpoint. You can use the Thread Status box to help you keep track of and manipulate all the threads in your application. To display the Thread status box, choose View|Threads from the main menu.
When a debug event occurs (breakpoint, exception, paused), the thread status view indicates the status of each thread. Right-click the Thread Status box to access commands that locate the corresponding source location or make a different thread current. When a thread is marked as current, the next step or run operation is relative to that thread.
The Thread Status box lists all your application's execution threads by their thread ID. If you are using thread objects, the thread ID is the value of the ThreadID property. If you are not using thread objects, the thread ID for each thread is returned by the call to BeginThread.
For additional details on the Thread Status box, see online Help.
pubsweb@inprise.com
Copyright © 1999, Inprise Corporation. All rights reserved.