The Win32 programming model is based on two high-level abstractions called processes and threads. A process is a running instance of an application that owns an address space of virtual memory as well as other various resources. A thread is a schedulable entity that's owned by one and only one process. The operating system recognizes each thread in every process and is responsible for scheduling threads for time in the system's processor(s), as shown in Figure 7-1.
Every thread gets its own call stack. The scheduler allocates processing cycles by giving each thread a time slice. When the time slice is over, the running thread is preempted and another thread is given a turn. A preempted thread can keep enough information on its call stack to remember what it was doing and how far it got before being switched out of the processor. When it gets another time slice, it can pick up where it left off. The abstraction of threads and the scheduler is powerful because it allows a single-processor computer to appear to be doing more than one thing at a time.
Figure 7-1. Every Win32 process owns one or more threads. Each thread has an associated call stack and can store data in local storage, which is private to the thread.
Each process begins its life with a primary thread. This system-created thread serves as the entry point into the application. In a typical Microsoft Windows-based application with a user interface, the primary thread is used to create the application's main window and to set up a message loop to monitor incoming messages sent by the operating system. For example, if a user clicks on a main window, the system sends a WM_MOUSEDOWN message to a message queue associated with the window. The primary thread pulls this message off the queue and responds to it. A thread that employs a loop to monitor the incoming messages in this manner is called a user interface thread.
When an application with a user interface is written in C or C++, someone must set up a message loop to monitor a message queue. Fortunately, Visual Basic programmers have always been shielded from having to deal with a message queue directly. The Visual Basic run time sets up a message loop in the background. Programming in Visual Basic is simple because the Visual Basic run time translates these system messages into Visual Basic events.
Once an application is up and running, the Win32 API lets you spawn additional threads using a Win32 API function named CreateThread. C and C++ programmers typically create secondary threads to carry out lower-priority background tasks. In most cases, the secondary thread doesn't monitor a message queue and therefore doesn't require much overhead. This type of thread is known as a worker thread. The obvious benefit of the second thread is that a background task can be run without blocking the responsiveness of the user interface.
Programmers who write multithreaded applications must exercise extreme caution. When two threads run at the same time in a single process, you can encounter problems that don't exist in a single-threaded process. In particular, global data that's accessed by multiple threads is vulnerable to inconsistency and corruption because of a phenomenon known as concurrency. In a preemptive multithreading environment such as Microsoft Windows NT, the scheduler switches threads out of the processor arbitrarily. There's no way to guarantee that a thread has completed its work. When another thread is switched into the processor, it can potentially see global data left in an invalid state by some other thread that was preempted in the middle of a series of changes.
For example, imagine that the variables x and y represent the position of a point. Assume that the initial position of the point is (10,10) and that thread A begins to change the position of the point to (20,20). If thread A is preempted after changing the x position but before changing the y position, the logical point is left in an invalid state. This invalid state is the position (20,10). The only two valid positions for the point are (10,10) and (20,20), but thread B might see the point at (20,10). As you can see, concurrency makes an application vulnerable to data inconsistency.
Multithreading also makes an application vulnerable to data corruption. Take a look at another example with a particularly unhappy ending. When thread A inserts a new entry into a linked list, it must modify a set of pointers to accomplish the task. If thread A is preempted in the middle of the operation, thread B can easily get hold of an invalid pointer when it tries to scan the list. When thread B tries to use the invalid pointer, the entire process will probably crash.
Multithreading makes it difficult to write code that is correct and robust. A Win32 programmer who wants to create a multithreaded application must lock and synchronize any global data that is vulnerable to inconsistency or corruption. The Win32 API exposes a set of synchronization primitives for this purpose. Critical sections, mutexes, semaphores, and events are all examples of Win32 synchronization objects. These objects are tricky to work with, but they let experienced programmers write safe, robust code that can benefit from multithreading.
The problems associated with concurrency arise only when two or more threads access the same global memory. You can avoid these problems by using local variables on the call stack. These local variables are private to a particular thread. The Win32 API also allows a thread to store persistent data in a private memory area. This type of memory is called thread-local storage (TLS). Using TLS solves many concurrency problems because you don't have to worry about synchronization.
Although TLS solves concurrency problems, it creates a few problems of its own. Data stored in TLS can be accessed only by the owning thread. Objects that store data in TLS generate a dependency on the thread that created them. As we'll see later in this chapter, Visual Basic objects are heavy users of TLS; therefore, every Visual Basic object has a dependency on the thread that created it. A Visual Basic object can never be accessed by any other thread. This condition is known as thread affinity. As you'll see later, thread affinity becomes a limiting factor in environments that use thread pooling.
Many programmers—especially those who write business logic in languages such as Visual Basic—don't think about concurrency and synchronization. They assume that their code will run in an environment with a single thread, or they don't think about threading at all. They never worry about synchronizing their data during method calls. They can't reap the benefits offered by a multithreaded environment, but they are generally much more productive because they never spend time on issues relating to concurrency.
As you can see, there are two very different ways to write code. You can write code that's thread-savvy or you can write code that's thread-oblivious. The creators of COM looked for a way to integrate both types of code into a single application. But they faced a large potential problem. If two components are integrated into a single application, a thread-savvy component might try to access a thread-oblivious component in a way that would make the thread-oblivious component behave unpredictably and crash.
One way to safely integrate these two components is to modify one to accommodate the other. You can make the thread-oblivious component more robust or make the thread-savvy component less dangerous. However, this approach is undesirable because it requires a modification of one of the code bases. COM solves the integration problem with an abstraction known as an apartment. Apartments allow a variety of components to interoperate without forcing anyone to change their thread management policy.
An apartment is an execution context that allows any component to assume that all other components are running with the same level of thread awareness. Here's how the model works. Every COM object is created within the context of a COM apartment, as shown in Figure 7-2. A Win32 thread must explicitly enter an apartment before creating and interacting with COM objects. Under strict rules defined by COM, a thread in one apartment cannot touch an object that lives in another apartment. If a method call is made from one apartment to another, the call must be remoted across a proxy/stub pair. The proxy/stub layer for interapartment communication is built the same way as for interprocess communication. As you'll see, an apartment can serialize every incoming method call in a manner that eliminates the possibility of concurrency.
Figure 7-2. In COM, every object must be created within the context of an apartment. Every COM-aware thread must also run within the context of an apartment.
COM currently defines two threading models, each based on a different type of apartment. A multithreaded apartment (MTA) provides an execution context for components that are thread-savvy. A single-threaded apartment (STA) is a safe execution context for running objects that were written without concern for locking and synchronization. STAs are helpful because they eliminate the need for programmer-assisted synchronization, but they're based on an invocation architecture that's less than optimal.
Two different sets of terminology are used to describe COM's threading models. The Win32 SDK documentation uses the terms multithreaded apartment and single-threaded apartment. Other authors and development tools use the terms free-threaded and apartment-threaded. A free-threaded component is written to run in an MTA. An apartment-threaded component is written to run in an STA. Whenever you hear the term apartment, you can infer that this means an STA. Every COM-aware thread must formally enter a specific apartment, so MTA and STA are really the more accurate terms.
A process that runs COM objects must have at least one apartment. Figure 7-3 shows three possible layouts for the apartments in a process. A process that runs a single apartment can employ either an STA or an MTA. An application can also run multiple apartments. A process is limited to a single MTA, but it can run many STAs. The MTA can run any number of threads and thread-savvy objects, so there's no need for more than one per process. However, a process can run multiple STAs to provide multithreading in an application that is built using thread-oblivious components.
A thread running in the MTA can directly execute a method on an object that is also running in the MTA. When a method is invoked on an MTA-resident object by a thread running in a different apartment, COM's underlying RPC layer services the call by dispatching a thread from a dynamically managed pool. Figure 7-4 shows how two incoming calls can be dispatched concurrently on an object. The MTA is a fast and responsive environment, but programmers are expected to write objects that are robust in the face of concurrency.
Figure 7-3. A process that runs COM objects contains one or more apartments. A process is limited to a single MTA, but it can have multiple STAs.
Figure 7-4. In a multithreaded apartment, when the process receives a method call bound for an object running in the MTA, COM's underlying RPC layer services it by dispatching a thread from a dynamic pool.
Visual Basic isn't yet capable of creating components that can run in the MTA. You must use some other tool or language (such as C++ or Java) to create a free-threaded component. A C++ programmer writing a free-threaded component must deal with concurrency by using Win32 locking primitives. This style of programming becomes increasingly complex and grungy, but it yields components that are potentially more responsive. Java offers support in both the language and the virtual machine (VM) for synchronization, so Java programmers can write MTA objects in a style that's far simpler than that in C++.
The Visual Basic team had a tough decision to make. They needed to decide whether their programmers should be exposed to synchronization issues. They decided that requiring programmer-assisted synchronization was unacceptable in a tool intended to provide the highest levels of productivity. Their decision means that Visual Basic's multithreaded capabilities derive entirely from running objects in multiple STAs. If you want to make the most of Visual Basic, you must understand how an STA works.
Figure 7-5 shows what life is like inside an STA. The RPC layer and an STA work together to serialize all incoming method calls, with help from a Windows message queue. This is the same kind of message queue used in a typical user interface application. When the COM library creates an STA, it also creates an invisible window that makes it possible for the application to set up a standard message loop. The RPC layer responds to an incoming method call by posting a message to the queue with the Win32 PostMessage function. The thread in the STA responds to method requests by servicing these messages on a first-in, first-out basis. When the STA is busy processing a call, incoming requests are queued up and must wait their turn. The STA's thread eventually works its way to the bottom of the queue and goes into idle mode when the queue is empty. While this invocation architecture requires a good deal of overhead, the STA eliminates the need for objects to be concerned about concurrency and synchronization.
Figure 7-5. In a single-threaded apartment, an STA serializes all inbound method calls by employing a Windows message queue. The RPC layer posts a message to the queue when a method is invoked.
An STA has two important limitations. First, it provides a roundabout way of invoking a method call. Posting and reading messages on the queue takes up valuable processing cycles, unlike the way in which calls are dispatched in the MTA. Second, the serialization of all inbound method calls has a significant effect on the application's responsiveness. Not only does an STA prevent multiple calls from executing concurrently on a single object, it also prevents calls from executing concurrently on any two objects in an STA. Unfortunately, when two objects live in the same STA, only one of them can execute a method at a time. It would be better if locking were done at the object level instead of at the apartment level. If you want two Visual Basic objects to run method calls concurrently, you must make sure that each object is created in a separate STA.
Every application that creates COM objects can control how apartments are created and used. This means that every COM-aware EXE should include code for creating and managing a set of apartments. A C++ programmer who creates an application or an out-of-process server creates apartments by making calls to the COM Library. The functions CoInitialize and CoInitializeEx are used to associate a running thread with an apartment. A thread can be associated with either an STA or the MTA. If the requested apartment doesn't exist, COM creates it automatically. You can create additional threads and associate them with either the MTA or a new STA. As you can see, the code behind an application has a strong influence on how apartments are laid out within the process.
One situation in which an apartment is created without the assistance of the hosting application occurs when a COM object is activated from an in-process DLL. If the client is running on a thread that's incompatible with the threading model of the object being created, COM transparently finds or creates a compatible apartment in which to create the new object. For example, the components that you distribute in an ActiveX DLL can't run in the MTA. So what happens when an object is activated from your DLL by a thread that is running inside the MTA? COM deals with this situation by silently creating a new STA to host your object. COM also loads the proxy/stub code required to bind the object back to the client.
Every coclass has an associated CLSID key for tracking information for activation in the Windows Registry. The Registry key CLSID\InprocServer32 can be marked with a ThreadingModel attribute to indicate which apartment types can be used to load objects activated from this CLSID. This attribute can be marked as Free, Apartment, or Both. The Free setting means that newly created objects can be loaded only into the MTA. The Apartment setting indicates that a new object must be loaded into an STA. In either case, if the creating thread isn't in a compatible apartment type, COM automatically creates (or finds) a compatible apartment. A setting of Both means that a new object can be loaded into the apartment of the caller regardless of whether it's an MTA or an STA.
In-process components that don't have a ThreadingModel attribute are always loaded into the first STA created within a process. This special STA is called the main STA. Classes that are configured to run there are often called single-threaded or main-threaded classes. Many programmers consider a CLSID without an explicit ThreadingModel attribute to be an obsolete, legacy component. The problem with such a component is that it can activate objects only inside the main STA. If a client is running in the MTA or any STA other than the main STA, a proxy/stub is introduced between the object and the client. An apartment-threaded component is almost always preferable to a main-threaded component because it allows a client in any STA to bind to an object without the overhead of the proxy/stub layer.