As is the case with any evolving environment, Windows presents an odd mix of the old and the new, the obsolete, outdated, and the modern, the state-of-the-art. Nowhere is it more evident than in its multitasking capabilities, in particular the differences in those capabilities between the various Win32 platforms.
The old: The cooperative multitasking environment of 16-bit Windows. Its antics and limitations survive intact in Win32s which, although it provides a rich implementation of the Win32 programming interface, nevertheless cannot alter the underlying operating system or eliminate its limitations.
The new: The multithreaded Windows NT operating system. An operating system that was designed fresh from the ground up, Windows NT offers a very robust multitasking capability, suitable for high-reliability applications (such as large corporate servers).
The odd: Windows 95. Here, the goal of the designers was as much to implement the new capabilities as to maintain 100 percent (well, close to 100 percent anyway) compatibility with the old 16-bit Windows environment. The result is an astonishing combination: Windows 95 delivers a surprisingly robust multitasking capability while at the same time doing an excellent job (sometimes better than 16-bit Windows itself) in maintaining compatibility with legacy applications. Naturally, this does not come without a price: Windows 95 suffers from some strange limitations, ever more likely to turn into annoying "gotchas" precisely because the system does such an excellent job delivering elsewhere. With its fluid multitasking capability, it may come as a surprise to the uninitiated that Windows 95 is just as likely to "freeze" because of an ill-behaved 16-bit application as Windows 3.1. (Although admittedly, Windows 95 does a lot better job recovering from such events.)
Because the differences are substantial, it pays to examine the multitasking capabilities of the three Win32 environments separately. But first, we turn our attention to some of the fundamental concepts essential to understanding multitasking in Windows.
Multitasking in general refers to an operating system's capability to load and execute several applications concurrently. A multitasking operating system is considered a robust and reliable one if it successfully shields concurrent applications from each other, making them believe that they alone "own" the computer and its resources. Furthermore, a well-written multitasking operating system also shields applications from each others' bugs; for example, if one application fails to perform array bounds checking and writes beyond the allocated boundaries of an array, the multitasking operating system should prevent this from overwriting the memory space of another application. To a large extent, multitasking operating systems rely on system hardware to implement these capabilities. For example, without the support of a memory management unit that generates an interrupt when an attempt is made to access memory at an illegal address, the operating system would have no way of knowing that such an attempt took place short of examining every single instruction in an application's code. This would be a very inefficient, time-consuming solutioncompletely impractical, in fact.
Another important aspect of multitasking is process scheduling. As most processors are capable of executing only a single stream of instructions at any given time, multitasking would obviously not be possible without the technique of context switching. A context switch, triggered by a specific event (such as an interrupt from a timer circuit or a call by the running application to a specific function), essentially consists of saving the processor context (instruction pointer, stack pointer, register contents) of one running program and loading that of another.
Other no less important aspects of multitasking involve the operating system's capability to provide contention-free access to various system resources (such as the file system, the display device), prevent deadlock situations, and provide mechanisms through which concurrently executing applications can communicate with each other and synchronize their execution.
The degree to which various operating systems provide multitasking support varies greatly. Traditional mainframe operating systems have provided robust support in all aspects of multitasking since decades ago. On the other hand, multitasking on desktop computers is a relatively new phenomenon, largely because these machines only recently became sufficiently powerful to execute several tasks at once efficiently. (That said, many programmers are surprised to learn that even vintage MS-DOS provides rudimentary support for multitasking; this is what enabled developers to write robust Terminate and Stay Resident, or TSR, applications.)
When examining the difference between the multitasking support in the various Win32 environments, we quickly find that the primary emphasis is on the scheduling mechanism employed.
In a cooperative multitasking environment (also referred to often as nonpreemptive) the operating system relies explicitly on applications to yield control by regularly calling a specified set of operating system functions. Context switching takes place at well-defined points during the execution of a program.
In a preemptive multitasking environment, the operating system can interrupt the execution of an application at any time. This usually happens when the operating system responds to a hardware event, such as an interrupt from a timer circuit. An application's flow of execution can be interrupted at any point, not only at predefined spots. This raises the complexity of the system. In particular, in preemptive multitasking environments, the possibility of reentrancy becomes a distinct issue; a program may be interrupted while it is executing an operating system function, and while it is suspended, another program may call into the same operating system function, or reenter the function before the call to it from the first program was complete.
Another phrase often heard in the context of multitasking and Windows is threads. Perhaps the best way to describe threads is this: While multitasking offers the capability of running several programs concurrently, threads make possible several concurrent paths of execution within the same program. The introduction of this mechanism adds a powerful capability to the application programmer's repertoire. The price (you knew there was a price to pay for this, didn't you?): problems that were previously the concern of operating system authors only, such as the problems associated with reentrancy and process synchronization, are now something application programmers must also worry about.
Windows 3.1 is a cooperative multitasking environment. 32-bit applications that run under Windows 3.1 using Win32s are subject to the same multitasking limitations as ordinary 16-bit applications.
NOTE: A very limited form of preemptive multitasking exists in Windows 3.1. This is what enables the concurrent execution of several MS-DOS programs in separate DOS windows.
In Windows 3.1, applications must regularly yield control to the operating system by calling one of the following functions: GetMessage, PeekMessage (without the PM_NOYIELD flag), and Yield.
It is fortunate that GetMessage is one of the yielding functions; thanks to this fact, applications that rely on a typical message loop for message processing need to do very little else to be well-behaved under the Windows 3.1 environment. However, the nature of the cooperative multitasking environment should never be forgotten; whenever a message triggers the execution of a time-consuming action, such as printing a large document or performing a lengthy calculation, the programmer is well-advised to include regular calls to Yield or PeekMessage in order not to "freeze" the operating system. Better yet, it should make an effort to actually process messages even during the time-consuming procedure; that way, the application's own window will also remain responsive. (For example, it would repaint itself if parts of it were uncovered due to an action of the user.)
Failing to cooperate with the operating system and other applications has more than mere cosmetic effects. As other applications will not have a chance to execute at all, odd things are bound to happen; the buffer of a communication application will overflow, a TCP/IP networking application will lose connection, timing-sensitive application will encounter a time-out condition, and so on. Worse yet, eventually the input buffers of Windows itself will overflow as well; ever heard that ugly rapid-fire beeping that is the response of a very sick Windows to any user-interface event (mouse button clicks, keyboard clicks, even mouse movements)?
There is very little excuse for writing an application that does not abide by the rules of cooperative multitasking. If your application is intended to run in the Win32s environment, abiding by these rules is a must; but, as we see momentarily, these practices are not a bad idea even in the preemptive Windows NT and Windows 95 environments.
I must admit, ever since the early versions of Windows NT, and despite some of the compatibility problems associated with it, switching from Windows 3.1 to Windows NT always felt like stepping out from a stuffy, overcrowded room to breathe some fresh mountain air.
This sensation was due in large part to NT's robust multitasking. Gone were the miseries of frozen applications, unresponsive keyboards, unsuccessful attempts to revive a system with the most drastic of methods, hitting Control+Alt+Delete. Instead, here was an operating system that always remained responsive, always offered a way to get rid of a pesky, ill-behaved program.
Windows NT provides preemptive multitasking for concurrent 32-bit processes. The case of 16-bit processes is special. These processes generally appear to Windows NT as a single process (the Windows On Windows, or WOW process), although beginning with Version 3.5, Windows NT enables 16-bit processes to run in a "separate memory space," meaning that a separate WOW process is started for them. Those 16-bit applications that share a WOW process, however, must abide by the rules of cooperative multitasking to enable each other to live. In other words, if a 16-bit process freezes, it will also freeze all other 16-bit processes with which it shares a process; however, it will not have any ill effect on other processes, including 16-bit processes that run as part of another WOW process.
Does preemptive multitasking in Windows NT mean that you can forget everything you learned about well-behaved Windows applications and start writing noncooperative code? Absolutely not, and here is the reason why.
Even though Windows NT is capable of wrestling control away from an uncooperative 32-bit application and thus it can enable other applications to run, it will not be able to process messages aimed at the uncooperative application. Thus, if an application fails to regularly check its message queue, it will still appear unresponsive, buggy to the user. The user will not be able to interact with the application at all. Clicking on the application's window to bring it to the front will fail, and the application will not redraw parts of its window when it is uncovered when another window is moved or closed.
To avoid this, an application should make every effort to regularly check its message queue and dispatch any messages in it, even when it is otherwise busy performing some lengthy task. While failing to do so no longer threatens the integrity of the system as a whole, it certainly serves as a recipe for a very "user-unfriendly" application.
Fortunately, there is another aspect of Windows NT multitasking that makes such lengthy processes much easier to implement. Unlike its 16-bit predecessor, Windows NT is a multithreaded operating system.
A Windows NT program can easily and inexpensively create new threads of execution. For example, if it needs to perform a lengthy calculation, that task can be delegated to a secondary thread, while the primary thread continues processing messages. A secondary thread can even perform user interface functions; for example, while an application's primary thread continues processing messages sent to its main window, the application can delegate the function of processing messages for a dialog to a secondary thread. (When using the Microsoft Foundation Classes, there is actually special terminology to distinguish threads that own windows and process messages and those that do not; they are referred to as user interface threads and worker threads, respectively.)
Windows 95 combines the best features of both Windows 3.1 and Windows NT and loses surprisingly little in terms of tradeoffs. (I guess you can tell from this that I like Windows 95. Indeed, I like it a lot.)
On the one hand, Windows 95 delivers a Windows NT like preemptive multitasking and multithreading capability. If anything, Windows 95 is perhaps even more responsive, thanks to code that is more optimized, more specifically tailored to the Intel family of processors than the portable code of Windows NT. On the other hand, Windows 95 delivers a remarkable degree of compatibility with legacy DOS and 16-bit Windows applications. And all this is delivered by an operating system that is only slightly more resource hungry than its predecessor. I witnessed this firsthand, when I successfully installed Windows 95 and Visual C++ 2.1 on my 8MB 486Sx25 noteguide computer.
This compatibility has been accomplished, in part, by incorporating large amounts of legacy code from Windows 3.1 into Windows 95. In other words, although Windows 95 is doubtless a 32-bit operating system, some code at its very heart is old-style 16-bit code. The obvious side effect of this is that some parameters that can have a full range of 32-bit values in Windows NT are restricted to 16 bits in Windows 95 (most notably, graphical coordinates). Another, less than obvious side effect has a direct consequence for multitasking under Windows 95.
Much of the Windows 3.1 legacy code has not been designed with reentrancy in mind. In other words, because 16-bit applications participated in cooperative multitasking, there was never a chance that one was interrupted in the middle of a system call; hence, there was no need to design mechanisms that would make it safe to repeatedly call system functions while a previous call was suspended, unfinished.
Because Windows 95 processes can be interrupted any time, Microsoft had two choices. The first was to rewrite Windows 3.1 system calls completely. Apart from being a monumental task, this approach would result in a loss of the advantage that importing Windows 3.1 legacy code represents, namely a high degree of backward compatibility. In effect, such a rewrite would result in another operating system; and that has been done, the result being Windows NT.
The other, much simpler solution is simply to protect the system while its 16-bit non-reentrant parts are executing. In particular, the Windows 95 solution means is that while one application is executing 16-bit code, all other applications are prevented from doing so.
This has a very noticeable effect in the case of 16-bit applications. You see, 16-bit applications are always running in 16-bit mode. What that means is that as long as a 16-bit application has control of the processor, no other application can execute the 16-bit code.
Which means that an uncooperative 16-bit application (one that fails to yield to the operating system, thus enabling other, 32-bit, processes to gain control) can just as effectively freeze the operating system as it did under Windows 3.1.
Fortunately, Windows 95 does a much better job of recovering. For example, it can kick out the offending process and do a good job cleaning up its mess without struggling with the stability and resource allocation problems that have plagued Windows 3.1.
The Win32 API contains a rich set of functions for accessing all the multitasking and multithreading features of 32-bit Windows. In some cases, these functions supersede or replace traditional UNIX or C library (or, for that matter, MS-DOS) functions. Other functions represent new areas of functionality. Yet another set of functions (for example, the yielding functions) is familiar to Windows 3.1 programmers.
The remainder of this chapter reviews some of the multitasking programming techniques.
As I mentioned earlier, authors of most simple Windows applications do not have to worry about cooperative multitasking. The typical message loop, shown in Listing 12.1, takes care of this problem. Every time the application becomes ready to process a new message, it calls the Windows GetMessage function. GetMessage, in turn, may not return immediately; instead, Windows may perform a context switch and pass control to another application.
Listing 12.1. Yielding in the message loop.
int WINAPI WinMain(...)
// Application initialization goes here
// Entering main message loop
while (GetMessage(&msg, NULL, 0, 0)) // This call yields!
// Message dispatching goes here
As I mentioned earlier, although in the 32-bit environment it is not strictly necessary for an application to yield cooperatively, it is a very good idea to continue doing so. Not only does this make the application more likely to remain compatible with Win32s, but more importantly, it ensures that the application itself remains responsive.
The example program shown in Listing 12.2 (resource file) and 12.3 (source file) demonstrates this technique. This is yet another simple example that can be compiled from the command line with the following instructions:
CL LOOP.C LOOP.RES USER32.LIB
Alternatively, you can create a Visual C++ project and add the files LOOP.C and LOOP.RC in order to compile from within the Development Studio.
Listing 12.2. Processing loop example resource file (LOOP.RC).
In this program, a processing-intensive loop is started when the user clicks in the client area of the application's main window. The processing in the DoIterate function is not particularly complex; it is simply incrementing the i variable and displaying the result repeatedly until the user stops the loop.
Before the iteration is started, however, the application displays a modeless dialog box. Moreover, it disables user interaction with the application's main window by calling the EnableWindow function. This has basically the same effect as using a modal dialog box with one crucial difference; we do not need to call the DialogBox function, and thus we retain control while the dialog box is displayed.
Inside the actual iteration loop, the function PeekMessage is called with great frequency. This ensures that the application yields control; but more importantly, it also ensures that the dialog through which the iteration can be aborted responds to user interface events.
NOTE: The PeekMessage call should only be used when the application actually performs background processing. Using PeekMessage instead of GetMessage prevents Windows from performing any "idle-time" processing such as virtual memory optimizations or power management on battery-powered systems. Therefore, PeekMessage should never be used in a general-purpose message loop.
While the previous technique can be used in programs intended for all Win32 platforms (including Win32s), it is somewhat cumbersome. For lengthy calculations of this kind, it is much easier to use a secondary thread in which the calculation can proceed uninterrupted, uncluttered with PeekMessage calls. Consider the example shown in Listing 12.4. This example can be compiled with the same resource file as the previous one, using identical command line instructions.
Listing 12.4. Processing in a secondary thread (LOOP.C).
Perhaps the most significant difference between the two versions of LOOP.C is that in the second version, the iteration loop within the DoIterate function no longer calls PeekMessage and DispatchMessage. It does not have to; for the DoIterate function is now called from within a secondary thread, created by a call to CreateThread in the function WndProc.
Instead, the primary thread of the application continues execution after creating the secondary thread, and returns processing messages in the primary message loop in WinMain. It is this primary loop that now dispatches messages for the dialog as well.
Of particular interest is the changed declaration of the global variable bDoAbort. It is through this variable that the secondary thread is notified that it should stop executing; however, the value of this variable is set in the primary thread when the user dismisses the dialog. Of course, the optimizing compiler is not aware of this fact; so it is quite likely, that the following construct:
becomes optimized in such a way that the value of bDoAbort is never reloaded from memory. Why should it be? Nothing inside the while loop modifies its value, so the optimizing compiler can legitimately keep this value in a register, for example, which means that any changes to the value stored in memory by another thread will not be noticed by this thread.
The C keyword volatile comes to our rescue. Declaring a variable volatile essentially tells the compiler that regardless of its optimization rules, the value of such a variable should be written to memory every time it is modified; and the value of such a variable should be reloaded from memory every time it is referenced. Thus, we ensure that when the primary thread sets a bDoAbort to a new value, the secondary thread will actually see this change.
Our second LOOP.C example contained a call to CreateThread. Calling this function is the preferred method for creating a secondary thread. The return value of this function, which in this simple example we unceremoniously discarded, is a handle to the new thread object.
The thread object encapsulates the properties of a thread, including, for example, its security attributes, priority, and other information. Thread manipulation functions refer to threads through thread object handles like the one returned by CreateThread.
Our secondary thread in LOOP.C used the simplest exit mechanism; when the function designated as the thread function in the call to CreateThread returns, the thread is automatically terminated. This is because exiting the thread function amounts to an implicit call to ExitThread.
NOTE: The thread object remains valid even after a thread terminates, unless all handles to it (including the one obtained through CreateThread) have been closed through a call to CloseHandle.
A thread's exit code (the return value of the thread function or the value passed to ExitThread) can be obtained through the GetExitCodeThread function.
A thread's priority can be obtained through GetThreadPriority and set through SetThreadPriority.
A thread can be started in a suspended state by specifying CREATE_SUSPENDED as one of the thread's creation flags in the call to CreateThread. A suspended thread can be resumed by calling ResumeThread.
Closely related to the creation and management of threads is the creation and management of entire processes.
MS-DOS programmers have long been using the exec family of functions for spawning new processes. Windows programmers used WinExec; while those from the UNIX world are more familiar with fork. In Win32, this functionality has been consolidated into the CreateProcess function.
The CreateProcess function starts an application specified by name. It returns a handle to a process object that can later be used to refer to the newly created process. The process object encapsulates many properties of the new process, such as its security attributes or thread information.
The process can be terminated by a call to the ExitProcess function. A process also terminates if its primary thread terminates.
Our little dance with the bDoAbort variable in the previous multithreaded example represents a simplistic solution to the problem of synchronizing two or more independently executing threads. Using a global variable served our purposes well, but may not be adequate in more complex situations.
One such situation arises when one thread has nothing to do while waiting for another thread to complete a particular task. If using a variable accessed from both threads were the only synchronization mechanism available to us, the waiting process would have to enter a loop, repeatedly checking this variable's value. If it is doing that with great frequency, the result is a lot of wasted processing capacity. This problem can be alleviated somewhat by inserting a delay between subsequent checks, for example:
while (!bStatus) Sleep(1000);
Unfortunately, in many cases this is not a satisfactory solution either; we may not be able to afford to wait tens or hundreds of milliseconds before acting.
The Win32 API provides a set of functions that can be used to wait until a specific object or set of objects becomes signaled. There are several types of objects to which these functions apply. Some are dedicated synchronization objects, andothers are objects for other purposes that nevertheless have signaled and nonsignaled states. Synchronization objects include semaphores, events, and mutexes.
Semaphore objects can be used to limit the number of concurrent accesses to a shared resource. When a semaphore object is created using the CreateSemaphore function, a maximum count is specified. Each time a thread that is waiting for the semaphore is released, the semaphore's count is decreased by one. The count can be increased again using the ReleaseSemaphore function.
The state of an event object can be explicitly set to signaled or nonsignaled. When an event is created using the CreateEvent function, its initial state is specified, and so is its type. A manual-reset event must be reset to nonsignaled explicitly using the ResetEvent function; an auto-reset event is reset to the nonsignaled state every time a waiting thread is released. The event's state can be set to signaled using the SetEvent function.
A mutex (mutual exclusion) object is nonsignaled when it is owned by a thread. A thread obtains ownership of a mutex object when it specifies the object's handle in a wait function. The mutex object can be released using the ReleaseMutex function.
Threads wait for a single object using the functions WaitForSingleObject or WaitForSingleObjectEx; or for multiple objects, using WaitForMultipleObjects, WaitForMultipleObjectsEx, or MsgWaitForMultipleObjects.
Synchronization objects can also be used for interprocess synchronization. Semaphores, events, and mutexes can be named when they are created using the appropriate creation function; another process can then open a handle to these objects using OpenSemaphore, OpenEvent, and OpenMutex.
Critical section objects represent a variation of the mutex object. Critical section objects can only be used by threads of the same process, but they provide a more efficient mutual exclusion mechanism. These objects are typically used to protect critical sections of program code. A thread acquires ownership of the critical section object by calling EnterCriticalSection and releases ownership using LeaveCriticalSection. If the critical section object is owned by another thread at the time EnterCriticalSection is called, this function waits indefinitely until the critical section object is released.
Another simple yet efficient synchronization mechanism is interlocked variable access. Using the functions InterlockedIncrement or InterlockedDecrement, a thread can increment or decrement a variable and check the result for zero without fear of being interrupted by another thread (which might also increment or decrement the same variable before the first thread has a chance to check its value). These functions can also be used for interprocess synchronization if the variable is in shared memory.
In addition to dedicated synchronization objects, threads can also wait on certain other objects. The state of a process object becomes signaled when the process terminates; similarly, the state of a thread object becomes signaled when the thread terminates. A change notification object, created by FindFirstChangeNotification, becomes signaled when a specified change occurs in the file system. The state of a console input object becomes signaled when there is unread input waiting in the console's input buffer.
The techniques involving multiple threads and synchronization mechanisms are available not only to programs using the graphical interface, but to other programs, such as console applications, as well. In fact, the C++ example in Listing 12.5 is exactly that, a simple console application (compile with cl mutex.cpp).
Listing 12.5. C++ example for the use of a mutex object.
hMutex = CreateMutex(NULL, FALSE, "MYMUTEX");
cout << "Attempting to gain control of MYMUTEX object...";
cout << '\n' << "MYMUTEX control obtained." << '\n';
cout << "Press ENTER to release the MYMUTEX object: ";
This unremarkable little program creates a mutex object and attempts to gain ownership of it. When only a single copy of it is being executed (in a Windows 95 or Windows NT DOS window), it does not exhibit any revolutionary behavior.
To really see what this application has been designed to demonstrate, open a second DOS window. Run this example in both. You will see that while the first copy successfully gains control of the mutex object, the second copy becomes suspended while attempting to do so. It remains in this suspended state as long as the first copy maintains control of the mutex object; but once the object is released through ReleaseMutex, the second copy's call to WaitForSingleObject returns and it in turn gains control of the object. In fact, there is no limit of the number of processes that can cooperate through this mechanism; you could launch as many copies of this program in separate DOS windows as you like (or as memory permits).
The reason the two instances of this program were able to refer to the same mutex object is they were both referring to the object by the same name. Using the same name identified the same global object. It is easy to see how named synchronization objects can be used in a similar fashion to synchronize threads and processes, guard access to limited resources, or provide a simple communication mechanism between processes.
Multitasking represents an operating system's ability to execute several processes concurrently. The operating system accomplishes this task by regularly performing a context switch to switch from one application to another.
In a cooperative multitasking system, applications must explicitly yield control to the operating system. The operating system does not have the capability to interrupt the execution of a noncooperating program.
In a preemptive multitasking system, the operating system can and does interrupt applications based on asynchronous events such as an interrupt from timer hardware. Such an operating system is more complex and has to deal with issues such as reentrancy.
Windows 3.1 and, consequently, Win32s are examples of a cooperative multitasking system. Windows NT and Windows 95 are preemptive multitasking systems, but Windows 95 does inherit some of the limitations of Windows 3.1 through the legacy 16-bit implementation of some of its internal functions.
Both Windows 95 and Windows NT are also multithreaded operating systems. Threads are parallel paths of execution within the same process.
Although programs in Windows 95 and Windows NT are no longer required to yield to the operating system, they should still process messages even while performing lengthy processing. This ensures that these applications remain responsive to user-interface events.
There are several methods for synchronizing the execution of threads and processes. In particular, the Win32 API provides access to special synchronization objects, such as semaphores, mutexes, and events.