|
From Events to ThreadsTo understand how Windows applications work internally, let's spend a minute discussing how multitasking is supported in this environment. You also need to understand the role of timers (and the Timer component) and of background (or idle) computing, as well as the ProcessMessages method of the Application global object. In short, we need to delve deeper into the event-driven structure of Windows and its multitasking support. Because this is a book about Delphi programming, I won't discuss this topic in detail, but I will provide an overview for readers who have limited experience with Windows API programming. Event-Driven ProgrammingThe basic idea behind event-driven programming is that specific events determine the control flow As this explanation shows, events are serialized; each event is handled only after the previous one is completed. When an application is executing event-handling code (that is, when it is not waiting for an event), other events for that application have to wait in a message queue reserved for that application (unless the application uses multiple threads). When an application has responded to a message and returned to a waiting state, it becomes the last in the list of programs waiting to handle additional messages. In every version of Win32 (9x, NT, Me, and 2000), after a fixed amount of time has elapsed, the system interrupts the current application and immediately gives control to the next program in the list. The first program is resumed only after each application has had a turn. This process is called preemptive multitasking. So, an application performing a time-consuming operation in an event handler doesn't prevent the system from working properly (because other processes have their time-slice of the CPU), but the application generally is unable even to repaint its own windows properly—with a very nasty effect. If you've never experienced this problem, try it for yourself: Write a time-consuming loop that executes when a button is clicked, and try to move the form or move another window on top of it. The effect is really annoying. Now try adding the call Application.ProcessMessages within the loop; you'll see that the operation becomes much slower, but the form will be refreshed immediately. As an example of the use of Application.ProcessMessages within a time-consuming loop (and the lack of this call), you can refer to the BackTask example. Here is the code using this approach (ignore the naïve technique for computing the sum of a given set of prime numbers): procedure TForm1.Button2Click(Sender: TObject); var I, Tot: Integer; begin Tot := 0; for I := 1 to Max do begin if IsPrime (I) then Tot := Tot + I; ProgressBar1.Position := I * 100 div Max; Application.ProcessMessages; end; ShowMessage (IntToStr (Tot)); end;
If an application has responded to its events and is waiting for its turn to process messages, it has no chance to regain control until it receives another message (unless it uses multithreading). This is a reason to use a timer: a system component that will send a message to your application whenever a specified time interval elapses. Using a timer is the only way to make an application perform operations automatically from time to time, even when the user is absent or not using the program (so that it is not processing any events). One final note—when you think about events, remember that input events (generated using the mouse or the keyboard) account for only a small percentage of the total message flow in a Windows application. Most of the messages are the system's internal messages or messages exchanged between different controls and windows. Even a familiar input operation such as clicking a mouse button can result in a huge number of messages, most of which are internal Windows messages. You can test this yourself by using the WinSight utility included in Delphi. In WinSight, choose to view the Message Trace, and select the messages for all the windows. Click Start, and then perform some normal operations with the mouse. You'll see hundreds of messages in a few seconds. Windows Message DeliveryBefore looking at some real examples, let's consider another key element of message handling. Windows has two different ways to send a message to a window:
The difference between these two ways of sending messages is similar to that between mailing a letter, which will reach its destination sooner or later, and sending a fax, which goes immediately to the recipient. Although you will rarely need to use these low-level functions in Delphi, this description should help you determine which one to use if you do need to write this type of code. Background Processing and MultitaskingSuppose you need to implement a time-consuming algorithm. If you write the algorithm as a response to an event, your application will be stopped completely during the time it takes to process that algorithm. To let the user know that something is being processed, you can display the hourglass cursor or show a progress bar, but this is not a user-friendly solution. Win32 allows other programs to continue their execution, but the program in question will appear to be frozen; it won't even update its own user interface if a repaint is requested. While the algorithm is executing, the application won't be able to receive and process any other messages, including paint messages. The simplest solution to this problem is to call the ProcessMessages and HandleMessage methods, discussed earlier. The problem with this approach, however, is that the user might click the button again or re-press the keystrokes that started the algorithm. To fix this possibility, you can disable the buttons and commands you don't want the user to select, and you can display the hourglass cursor (which technically doesn't prevent a mouse-click event, but does suggest that the user should wait before doing any other operation). For some low-priority background processing, you can also split the algorithm into smaller pieces and execute each of them in turn, letting the application fully respond to pending messages in between processing the pieces. You can use a timer to let the system notify you once a time interval has elapsed. Although you can use timers to implement some form of background computing, this is far from a good solution. A better technique would be to execute each step of the program when the Application object receives the OnIdle event. The difference between calling ProcessMessages and using the OnIdle event is that calling ProcessMessages gives your code more processing time. Calling ProcessMessages lets the program perform other operations while a long operation is being executed; using the OnIdle event lets your application perform background tasks when it doesn't have pending requests from the user. Delphi MultithreadingWhen you need to perform background operations or any processing not strictly related to the user interface, you can follow the technically most correct approach: spawn a separate thread of execution within the process. Multithreading programming might seem like an obscure topic, but it really isn't that complex, even if you must consider it with care. It is worth knowing at least the basics of multithreading, because in the world of sockets and Internet programming, there is little you can do without threads. Delphi's RTL library provides a TThread class that will let you create and control threads. You will never use the TThread class directly, because it is an abstract class—a class with a virtual abstract method. To use threads, you always subclass TThread and use the features of this base class. The TThread class has a constructor with a single parameter (CreateSuspended) that lets you choose whether to start the thread immediately or suspend it until later. If the thread object starts automatically, or when it is resumed, it will run its Execute method until it is done. The class provides a protected interface, which includes the two key methods for your thread subclasses: procedure Execute; virtual; abstract; procedure Synchronize(Method: TThreadMethod); The Execute method, declared as a virtual abstract procedure, must be redefined by each thread class. It contains the thread's main code—the code you would typically place in a thread function when using the system functions. The Synchronize method is used to avoid concurrent access to VCL components. The VCL code runs inside the program's main thread, and you need to synchronize access to VCL to avoid re-entry problems (errors from re-entering a function before a previous call is completed) and concurrent access to shared resources. The only parameter of Synchronize is a method that accepts no parameters, typically a method of the same thread class. Because you cannot pass parameters to this method, it is common to save some values within the data of the thread object in the Execute method and use those values in the synchronized methods.
Another way to avoid conflicts is to use the synchronization techniques offered by the operating system. The SyncObjs unit defines a few VCL classes for some of these low-level synchronization objects, such as events (with the TEvent class and the TSingleEvent class) and critical sections (with the TCriticalSection class). (Synchronization events should not be confused with Delphi events, as the two concepts are unrelated.) An Example of ThreadingFor an example of a thread, you can refer again to the BackTask example. This example spawns a secondary thread for computing the sum of the prime numbers. The thread class has the typical Execute method, an initial value passed in a public property (Max), and two internal values (FTotal and FPosition) used to synchronize the output in the ShowTotal and UpdateProgress methods. The following is the complete class declaration for the custom thread object: type TPrimeAdder = class(TThread) private FMax, FTotal, FPosition: Integer; protected procedure Execute; override; procedure ShowTotal; procedure UpdateProgress; public property Max: Integer read FMax write FMax; end; The Execute method is very similar to the code used for the buttons in the BackTask example listed earlier. The only difference is in the final call to Synchronize, as you can see in the following two fragments: procedure TPrimeAdder.Execute; var I, Tot: Integer; begin Tot := 0; for I := 1 to FMax do begin if IsPrime (I) then Tot := Tot + I; if I mod (fMax div 100) = 0 then begin FPosition := I * 100 div fMax; Synchronize(UpdateProgress); end; FTotal := Tot; Synchronize(ShowTotal); end; procedure TPrimeAdder.ShowTotal; begin ShowMessage ('Thread: ' + IntToStr (FTotal)); end; procedure TPrimeAdder.UpdateProgress; begin Form1.ProgressBar1.Position := fPosition; end; The thread object is created when a button is clicked and is automatically destroyed as soon as its Execute method is completed: procedure TForm1.Button3Click(Sender: TObject); var AdderThread: TPrimeAdder; begin AdderThread := TPrimeAdder.Create (True); AdderThread.Max := Max; AdderThread.FreeOnTerminate := True; AdderThread.Resume; end; Instead of setting the maximum number using a property, it would have been better to pass this value as an extra parameter of a custom constructor; I've avoided doing so only to remain focused on the example of using a thread. You'll see more examples of threads in other chapters—particularly Chapter 19, "Internet Programming: Sockets and Indy," which discusses the use of sockets. |
|
Copyright © 2004-2025 "Delphi Sources" by BrokenByte Software. Delphi Programming Guide |
|