From Events to Threads

From Events to Threads

To 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 Programming

The basic idea behind event-driven programming is that specific events determine the control flowof the application. A program spends most of its time waiting for these events and provides code to respond to them. For example, when a user clicks one of the mouse buttons, an event occurs. A message describing this event is sent to the window currently under the mouse cursor. The program code that responds to events for that window receives the event, processes it, and responds accordingly. When the program has finished responding to the event, it returns to a waiting or "idle" state.

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):

There is a second alternative to calling ProcessMessages: the HandleMessage function. There are two differences: HandleMessage processes at most one message each time it is called, whereas ProcessMessages keeps processing messages in the queue; and HandleMessage also activates idle time processing, such as action update calls.

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 Delivery

Before 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:

PostMessage API Function Places a message in the application's message queue. The message will be handled only when the application has a chance to access its message queue (that is, when it receives control from the system), and only after earlier messages have been processed. This is an asynchronous call, because you do not know when the message will be received.

SendMessage API function Executes message-handler code immediately. SendMessage bypasses the application's message queue and sends the message directly to a target window or control. This is a synchronous call. This function even has a return value, which is passed back by the message-handling code. Calling SendMessage is no different than directly calling another method or function of the program.

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 Multitasking

Suppose 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 Multithreading

When 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:

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.

Note

Delphi 7 includes two new versions of Synchronize that allow you to synchronize a method with the main thread without calling it from the thread object. Both the new overloaded Synchronize and StaticSynchronize are class methods of TThread and require a thread as parameter.

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 Threading

For 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:

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:

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.