Sharing Threads

[Previous] [Next]

Threading is an important issue for any middle-tier application that will experience moderate to heavy traffic. High-traffic periods typically occur when online users are issuing many client requests per second. A scalable process must be able to handle each request with an acceptable level of responsiveness. A process isn't considered scalable if its responsiveness and overall throughput decrease in a linear fashion as the number of requests per second increases.

A process that's based on a single thread can't scale because it can't execute more than one method at a time. In other words, it doesn't allow for concurrency. During heavy-traffic periods, incoming requests are simply queued up and run serially. Furthermore, a single-threaded model is incredibly limiting when a process is running on a computer with multiple processors. The process simply can't take advantage of what the hardware has to offer.

A process that spawns a new thread for each client also has problems with respect to scalability. Physical threads must be created and torn down as clients come and go. Moreover, as the number of clients (and threads) increases, the operating system must spend a larger percentage of its time switching threads in and out of its processors. As this administrative overhead increases, the process's overall throughput decreases.

Years of experience led the industry to an important conclusion: To attain the highest levels of scalability, a middle-tier process must provide some type of thread-pooling scheme. The goal of such a scheme is to create an optimized balance between higher levels of concurrency and more efficient resource usage. However, writing the code to manage a pool of threads and efficiently dispatch them across a set of clients isn't a trivial undertaking.

Fortunately, COM+ provides a built-in scheme for thread pooling, and it does so in a way that's transparent to you, the programmer. You never have to think about the details of how the COM+ thread pool manager works when you write Visual Basic components. In essence, you write your components from a single-threaded perspective and let the COM+ runtime spread out your objects across a set of threads at runtime.

While the COM+ threading scheme is largely transparent, you must keep in mind several important threading concepts when you design Visual Basic components. For that reason, I'll take a little time to cover the fundamental threading concepts of the Microsoft Windows operating system and of COM. This will, in turn, lead to a discussion of the COM+ thread-pooling scheme, which is based on an abstraction called an activity. My goal is to give you the information you need to take advantage of the activity-based concurrency model and avoid programming techniques that diminish the scalability of your applications and the correctness of your code.

A Win32 Threading Primer

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 various other 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 processors, 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 combination 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.

click to view at full size.

Figure 7-1 Every Win32 process owns one or more threads. Each thread has an associated call stack and can store data in a private area known as thread-local storage (TLS).

Each process begins its life with a primary thread. This system-created thread serves as the entry point into the application. In a typical 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 pump 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 sets up a pump to monitor the incoming messages in this manner is called a user interface thread.

Often when a user interface is written in C or C++, someone must set up the pump by writing a loop to monitor the message queue associated with the window. Fortunately, Visual Basic programmers have always been shielded from having to deal with a message pump directly. The Visual Basic runtime sets up a message pump in the background. Creating a Visual Basic application with a user interface is simple because the Visual Basic runtime translates these Windows 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 as much overhead. This type of thread is often referred to 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.

Concurrency and synchronization

If you write multithreaded applications by calling CreateThread, you must exercise extreme caution. When two threads run concurrently in a single process, you can encounter problems that don't exist in a single-threaded process. In particular, shared memory 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 Windows 2000, 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 might see shared memory 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. The initial position of the point is (10,10), and 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—the position (20,10). The only valid positions for the point are (10,10) and (20,20), but thread B might see the point as (20,10). As you can tell, 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 shared memory that's 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 data items in shared memory. You can avoid these problems by using local variables on the call stack instead of using shared memory. 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 known as 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. Visual Basic objects are heavy users of TLS, so 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.

COM's Threading Models

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 under a single thread of control or else they don't think about threading at all. They never worry about synchronizing their data during method calls. They're generally much more productive than programmers who spend time on issues relating to concurrency.

As you can see, you can write code in two very different ways. You can write a component with synchronization code that's thread-savvy or you can write a component from the single-threaded perspective. The creators of COM looked for a way to integrate the two types of components in a single application. More specifically, they looked for a way to integrate components that didn't include their own synchronization code in a multithreaded process. Their solution was to offer system-provided synchronization to any object that needs it.

COM can provide automatic synchronization through an abstraction known as an apartment. Apartments allow components without custom synchronization code to safely run in multithreaded processes. Moreover, apartments allow objects that exhibit thread affinity, such as those created with Visual Basic, to run safely in multithreaded processes.

In COM, every object must be created in a specific apartment and every apartment must exist in a specific process. You can view an apartment as a set of objects with compatible synchronization requirements. However, that's not the entire story. Every COM-aware thread is associated with exactly one apartment, so you can define an apartment as a set of objects and threads in a process.

You should note that apartments have been around longer than MTS or COM+. This means that apartments were part of the programming model before the introduction of contexts. With the release of Windows 2000, you can define an apartment as a set of contexts running in a process. That means that processes can be partitioned into apartments and that apartments can be partitioned into contexts.

Before Windows 2000, there were only two types of apartments: the multithreaded apartment (MTA) and the single-threaded apartment (STA). The MTA provides an environment for components with custom synchronization code. An STA is a safer environment for running objects that were written without concern for locking and synchronization. STAs were designed to eliminate the need for programmer-assisted synchronization.

Over the last few years, two different sets of terminology have been used to describe these two threading models. The Win32 SDK documentation uses the terms multithreaded apartment and single-threaded apartment, while other authors and development tools use the terms free-threaded and apartment-threaded. A free-threaded component is written to run in the MTA, and an apartment-threaded component is written to run in an STA. When you hear the term apartment, you can generally infer that this means an STA.

So, how does an STA prohibit concurrency? As its name implies, an STA is based on a single thread of execution. This thread is the only one that's allowed to directly touch any of the objects in the STA. This restriction is all that's needed to prohibit concurrency and provide a safe environment for objects that exhibit thread affinity. However, an STA must be capable of processing method calls originating from other apartments. To meet this requirement, the STA gets some assistance from the underlying RPC layer and a Windows message queue.

When the COM runtime creates an STA, it also creates an invisible window. Note that the STA doesn't really need the window; it just needs the message queue associated with the window. This queue allows the STA to set up a standard message pump, as shown in Figure 7-2. The RPC layer responds to method calls targeted for the STA by posting a standard Windows message to this queue using the Win32 PostMessage function. The thread in the STA runs a message pump just like the primary thread in an application with a user interface does. This allows the STA to respond to method requests by processing these messages on a first-in, first-out basis.

click to view at full size.

Figure 7-2 Every STA includes a user interface thread, a Windows message queue, and a message pump. This architecture allows STAs to serialize incoming calls and run objects that exhibit thread affinity.

When the STA is busy processing a call, other 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 programmers to be concerned about concurrency and synchronization.

When the Visual Basic team originally added multithreaded COM support to their product, they had a tough decision to make. They had 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. They decided that all Visual Basic objects would be required to run according to the STA model. They also made the assumption that it was acceptable to use TLS in the code that's automatically built into every Visual Basic component. This is why Visual Basic objects exhibit thread affinity.

You must use another language, such as C++, to create free-threaded components. A C++ programmer writing a free-threaded component must typically deal with concurrency by writing custom synchronization code using Win32 locking primitives. This style of programming becomes increasingly complex and grungy, but it yields components that are potentially faster and more responsive.

Windows 2000 introduced a third threading model called the thread-neutral apartment (TNA). Unfortunately, Visual Basic programmers can't take advantage of this new model because of the issues relating to thread affinity. But this new model is definitely of interest to C++ programmers, especially those who have previously built components for the MTA.

Before Windows 2000, the rules of COM required a thread switch in almost all situations for calls that cross over an apartment boundary. While these rules help to guarantee the synchronization scheme built into the STA model, they are unnecessarily taxing for objects running in the MTA. In particular, a call from a client in an STA to an object in the MTA can require an expensive and unnecessary thread switch. An object running under the TNA model, however, can be directly accessed by any thread in a process. For this reason, the TNA is the preferred threading model for Windows 2000. Of course, you can use this model only if your tools and language are capable of creating objects that don't exhibit thread affinity.

Visual Basic components and the ThreadingModel attribute

As you know, each component has an associated CLSID key in the Windows Registry. The SCM uses the information in this key during object activation. The Registry key CLSID\InprocServer32 can be marked with a ThreadingModel attribute to indicate which apartment types are compatible with the component. A Visual Basic component has either a ThreadingModel attribute of Apartment or no threading model attribute at all, as shown in Figure 7-3. A few other settings are possible for the ThreadingModel attribute, but they're not pertinent to developing with Visual Basic.

click to view at full size.

Figure 7-3 You can specify an apartment type for a component by providing a ThreadingModel attribute in the CLSID\InprocServer32 key. Components without this attribute are considered single-threaded.

You can explicitly assign the ThreadingModel attribute to the components in an ActiveX DLL project by adjusting the Threading Model setting on the General tab of the Project Properties dialog box. Two settings are possible. The default setting of Apartment Threaded ensures that each component is marked with a ThreadingModel attribute of Apartment. This setting indicates that a new object must be loaded into an STA. However, the new object can be loaded into any STA. As long as the object's creator is running in an STA, the object is created in the same apartment, which means that the two can communicate without a thread switch.

If you specify the other Threading Model setting, Single Threaded, each component is configured without a ThreadingModel attribute. When the SCM creates an object from a component that doesn't have a ThreadingModel attribute, it does so in the safest possible manner. Because the SCM can't make any assumptions about the component, it creates every object on the same thread. More specifically, it creates every object in the first STA created in the hosting process. The first STA created is known as the main STA. A component that lacks a ThreadingModel attribute is often called single-threaded or main-threaded.

Single-threaded DLLs are problematic because they cause unnecessary thread switches. If a creator is running in any STA other than the main STA, a thread-switching proxy/stub layer must be inserted between the creator and the object. Single-threaded DLLs are especially undesirable in environments such as COM+ and IIS. Apartment-threaded DLLs are almost always preferable because they allow a client in any STA to bind to an object in that same STA, which avoids the overhead of a thread-switching proxy/stub layer.

Never Call CreateThread from Visual Basic

A Win32 thread can't participate in COM programming without making an explicit COM library call to either CoInitialize or CoInitializeEx. These functions associate a thread with an apartment. It's illegal and usually fatal for a thread to participate in any COM-related activities before calling one of these functions. Fortunately, the COM+ runtime makes this call for you when it creates a thread. If you're creating a Standard EXE or an ActiveX EXE, the Visual Basic runtime makes all of the required calls to CoInitialize behind the scenes.

Working directly with Win32 threads and COM apartments is difficult and, for all practical purposes, beyond the capabilities of the Visual Basic programming language. However, when Visual Basic 5 first shipped, many unfortunate programmers tried to directly call the CreateThread function in the Win32 API by using Visual Basic's new AddressOf operator. The programmers found that they could successfully call CreateThread but that their applications often died strange and mysterious deaths.

The problem with calling CreateThread is that it creates a new Win32 thread that knows nothing about COM or apartments. It's therefore illegal to use this thread to invoke a method on a COM object. This creates quite a problem because all Visual Basic objects, including forms and controls, are also COM objects. Such an object can be directly accessed only by the thread running in the object's apartment. When the newly created thread tries to change a value in a text box, the application crashes. This is a pretty good reason to never use CreateThread directly from Visual Basic code.

A determined Visual Basic programmer with knowledge of COM's threading models might try to take things to the next level. You can associate a thread with an STA by calling a function such as CoInitialize directly. However, once you associate the thread with an apartment, the complexity increases. For instance, you're typically required to write code for marshaling interface references (which are really interface pointers at this level) from one apartment to another. If you pass the interface references in a COM method call, the proxy and the stub are created automatically. However, if you pass them using another technique, the requirements increase dramatically. You must make two complicated calls to the COM library—a call to CoMarshalInterface to create a stub in the object's apartment and a call to CoUnmarshalInterface to create a proxy in the caller's apartment. What's more, you also have to know enough about COM to decide when these calls are necessary. Add in the complexities related to the rules of COM+ and contexts, and you should come to an important conclusion: Never call CreateThread from Visual Basic. If you really need to spawn your own threads, you should be using C++ and you should know a great deal about the underlying plumbing of COM.

I must admit that this entire sidebar is just an opinion. Some of the icons of the Visual Basic world, such as Matt Curland, advocate spawning threads directly from Visual Basic code in the manner I just described. Matt can tell you about the details of this undertaking in a book he's written titled Advanced Visual Basic 6: Power Techniques for Everyday Programs (published by Addison Wesley).

If you feel comfortable with all the COM details I covered in Chapter 3 and you're looking for more in-depth coverage, Matt's book can take you to the next level. Matt has worked on the Visual Basic team for quite a few years and he has intimate knowledge of how the Visual Basic runtime interacts with the COM library. He describes many techniques that involve calling into the COM library directly from Visual Basic code.

The activity-based concurrency model

Apartments were the primary mechanism for system-provided synchronization in earlier versions of COM. In the COM+ programming model, system-provided synchronization is the responsibility of a new abstraction called an activity. An activity can be defined as a set of contexts whose objects are protected from concurrent access. Creating one or more objects in an activity relieves you from having to write custom synchronization code. For this reason, you can say that an activity is a synchronization domain. Or you can say that an activity represents a logical thread.

An activity does the same job as an STA with respect to serializing method calls. However, an activity accomplishes its mission more efficiently. It doesn't need to pin each object to a specific thread. Instead, the COM+ runtime prohibits concurrency in an activity through an internal locking scheme.

The COM+ runtime tracks activities on a per-process basis using an internal data structure. Each activity has an identifying GUID and an exclusive lock. Any thread must acquire this lock before entering an activity and accessing any of its objects. Once a thread has the lock, all other threads will block when they attempt to enter the activity. Once a thread has entered an activity, it must release the lock on its way out. This allows the next thread to enter. As you can see, this locking scheme prevents concurrency much as an STA does. However, it does so without restricting access to a single thread or requiring an invisible window along with its associated message queue. In essence, it provides automatic synchronization without all the baggage associated with an STA.

Apartments are still part of the COM+ programming model, but their role has been scaled back. Now they're used primarily to manage objects that exhibit thread affinity. An activity doesn't restrict access to a single thread. Only an STA can guarantee that an object with affinity to one specific thread is never touched by another thread. As a consequence, every Visual Basic object running in a COM+ application must live in an STA. A Visual Basic object should also live in an activity as well (although it's not a requirement).

The Synchronization attribute

The association between a context and an activity is established when the context is created. As you know, the COM+ runtime creates a new context when it creates an object that needs its own contextual information. Each configured component has a Synchronization attribute that tells the COM+ runtime whether a new context should be associated with an activity. If the COM+ runtime creates a new context that needs an activity, the Synchronization attribute also indicates whether the context should inherit the creator's activity or get a new activity of its own. Table 7-1 describes how COM+ matches up new objects and contexts to activities.

In practice, you don't need to think too much about the differences between the synchronization settings when you create components with Visual Basic. You should use the default setting Required. I'll explain why in a moment. C++ programmers, on the other hand, can create components without thread affinity that don't need to run in an STA. This offers many more options with respect to how the Synchronization attribute is set. Therefore, C++ programmers typically choose a synchronization setting based on whether they want automatic synchronization. (The issues involved in designing COM+ components with C++ are a topic for another book.)

Table 7-1 How COM+ Matches New Objects and Contexts to Activities

Synchronization Attribute Method Used for Matching
Not Supported The object is created in a context that has no activity.
Supported If the creator is running in an activity, the object is created in a context that shares that activity. If the creator isn't running in an activity, the object is created in a context that has no activity.
Required If the creator is running in an activity, the object is created in a context that shares that activity. If the creator isn't running in an activity, the object is created in a context with a new activity.
Requires New The object is created in a context with a new activity.
Disabled The SCM ignores the synchronization property when deciding whether the object requires its own context. The object will be in an activity only if it's placed in the creator's context and that context has an activity.

Moving from MTS
All MTS Objects Run in an Activity

Activities were first introduced with MTS. In MTS, every object must run in an activity. You can think of every MTS component as having a synchronization setting of Required. MTS also requires every object to run in an STA. This is pretty much the way things are for Visual Basic objects in COM+. However, unlike COM+, MTS makes it impossible to take advantage of C++ components with more sophisticated threading capabilities.

Apartment-threaded objects and activities

Figure 7-4 shows the layout of Visual Basic objects in a typical server application process with multiple clients. The basic premise behind the COM+ concurrency model is that each client should get its own activity. You should generally avoid designs in which a single client requires more than one activity. You should always avoid designs in which two clients map to the same activity.

click to view at full size.

Figure 7-4 Each client should be associated with exactly one activity. The COM+ runtime binds logical activities to physical STA threads when apartment-threaded objects are involved.

When would a client be associated with multiple activities? It can happen if a client running outside the COM+ runtime creates two separate objects. Note that the client isn't running in an activity. If the client creates two objects from components that require synchronization, the COM+ runtime creates a separate activity for each object. A better approach is to have the client create the first object and then have this object create the second object. The second object is created in the same activity as the first object as long as its component has a synchronization attribute of Required.

Why is it important for both objects to share the same activity? It has to do with consuming fewer resources and avoiding unnecessary thread switches. With respect to resource utilization, it should be obvious why you want to limit each client to a single thread. A client doesn't need more than one. It's also much faster for one object to call another without a thread switch.

If object A will be calling object B, you should make sure they're both in the same STA. The COM+ runtime guarantees that if two apartment-threaded objects are running in the same activity and in the same process, they will also run in the same STA. This is the main reason that you should always use the Required synchronization setting.

Now you know that it's bad for one client to own more than one activity. As it turns out, it's even worse for two or more clients to map to one activity. The COM+ architects made the assumption that each activity is associated with exactly one client. When a client calls into an activity, it should wait for the call to return before issuing another call. This model wasn't designed to accommodate situations in which two or more clients make calls into the same activity independently. It's equally bad if a single client application tries to make calls into the same activity using two separate threads. The main rule to keep in mind is that you should never make a call into an activity before the previous call returns.

An object that is shared across multiple clients is often called a singleton. Developers typically design with singletons in order to share data across multiple clients. While the use of singleton objects might be acceptable in some single-user scenarios, you should definitely avoid it in COM+ applications. Every client should get its own private objects. If multiple clients require access to the same data, you should store this data in shared memory or in a database. You can then devise a design in which each client creates a private object that accesses this shared data.

You should note that COM+ doesn't restrict an activity to a single process. Figure 7-5 shows an example of an activity that spans processes. If object A is running in an activity in server application 1 and it creates object B in server application 2, the new object and its context inherit the activity of the creator. This model, of course, assumes that the component used to create object B requires synchronization.

click to view at full size.

Figure 7-5 An activity can span process and computer boundaries.

As you can see, COM+ flows contextual information about the activity from object A to object B during activation. In Chapter 8, you'll see that objects must be in the same activity in order to be in the same COM+ transaction. But it's possible for two objects to participate in the same transaction even when they're running on different computers. They just need to be in the same activity.

You should be aware of one limitation of activities. The COM+ runtime doesn't guarantee activitywide synchronization across processes. The locking scheme used by COM+ works only within the scope of a single process. It would be too expensive for COM+ to check locks in other processes before allowing a thread to enter an activity. In Figure 7-5, what would happen if one client were to call into object A at the same time that another client calls into object B? COM+ wouldn't be able to prevent two calls from executing in an activity at the same time. However, this limitation should never cause a problem because your designs should never allow for this situation to occur. As long as you never allow two separate clients to map to the same activity, the single-process locking scheme shouldn't be an issue.

The STA thread pool

Each COM+ server application maintains two separate thread pools. One thread pool services STA objects, and the other one services all the other objects. As a Visual Basic programmer, you should obviously be more concerned with the STA thread pool. The COM+ runtime sets up the STA thread pool based on the following rules:

  • The initial pool size is 7 plus the number of processors.
  • The maximum pool size is 10 times the number of processors.
  • The thread pool size increases based on the number of queued requests.
  • During times with less traffic, the thread pool size decreases based on system-supplied code triggered by idle timers.

Moving from MTS
Thread Pool Size in MTS

In MTS 2, each server package process maintains a single pool of STA threads. This pool has an initial size of 1 and a maximum size of 100. The pool size increases based on the number of activities. Unlike the situation in COM+, the MTS thread pool never decreases in size.

After looking at these rules, many developers ask this question: How can my application scale if it's limited to 10 threads? Your intuition might tell you that increasing the number of threads will always make things better. However, this simply isn't the case. You can scale only so far when your application is running on a computer with a single processor. Naively increasing the number of threads to 50 or 100 or 1000 will hurt the overall throughput of your application.

The thread-pooling scheme that's built into COM+ is self-tuning with respect to the number of available processors on the host computer. The best way to accommodate a growing user base is to upgrade your hardware. Acquiring a new server computer that has more processors or faster processors or both is the best place to start. Obviously, you should also make sure the computer has more than enough physical memory to serve the expected number of users.

Here's another question that everyone always asks: How many users can a COM+ application handle? The answer is, of course, that it depends. With the right hardware in place, an application can handle several hundred users. Later in this book, I'll talk about using HTTP as opposed to COM and RPC for client-to-server communication. As you'll see, HTTP has many scaling advantages over RPC, which is especially important when an application has thousands or tens of thousands of users. For this reason, many distributed applications use COM and RPC for server-to-server communication and rely on HTTP for client-to-server communication.

Activities and STA threads

You should observe that your application has a finite number of STA threads to work with. At first, COM+ assigns each activity to its own thread. However, when the number of activities exceeds the number of STAs, COM+ must map multiple activities to the same STA. It's important to note that a client is pinned to an STA thread when an activity is created. In an application with many users, each STA has its own set of clients.

Remember that STAs are physical threads, while activities are logical threads. When two activities share the same STA, the COM+ runtime manages the multiplexing of STAs in and out of activities. The multiplexing provided by COM+ is pretty sophisticated.

Let's look at an example to see how things work. Take a look at Figure 7-6. Assume that client 1 calls a method on object A. A split-second later, client 2 attempts to call a method on object B. Object B is in a different activity than object A, but they share the same STA.

click to view at full size.

Figure 7-6 An activity releases control of its STA thread when it returns or when it makes an outbound call.

While the STA thread is busy executing the method call for client 1, the call from client 2 is blocked. You should see that this blocking isn't enforced by activity-based locks. The blocking is due to the single-threaded nature of the STA. But what happens if object 1 makes an outbound call to another process while executing its method? Perhaps object 1 makes a COM call to another object in a different server application or an OLE-DB call to a database server. Will the call from client 2 block while object 1 is waiting for this outbound call to return? Fortunately, the answer is no. The multiplexing scheme is smart enough to free up the STA thread. Once the call from object 1 leaves the STA, another activity can take control of the thread.

These locking semantics are fairly complex. The call to object B can start executing after the call to object A but still finish before it. This behavior helps to improve concurrency, but it relaxes the rules of synchronization. When objects in an STA make outbound calls, the STA doesn't guarantee that calls will be serialized. However, calls are serialized from the perspective of each activity.

I want to show one other example of how the relaxed rules of synchronization might catch a programmer off guard. Let's say a programmer creates an object from a Visual Basic component that doesn't support synchronization. The object is created in an STA, but it doesn't have an associated activity. Now assume that two different clients hold references to this object. (I know you'd never share an object across two clients, but imagine that you have a friend who does this.)

Assume that client 1 makes a method call on the object. When this method starts to execute, it changes a module-level variable to 5 and then makes an outbound call. Now let's say client 2 calls a method that executes while the object is waiting for this outbound call to return. If the method called by client 2 changes the module-level variable to 6, client 1 sees inconsistent data.

When the outbound call was issued, the variable value was 5. When the outbound call returned, the value was 6. The call from client 1 wasn't run in a serialized fashion. If the object had been created in an activity, however, there wouldn't be any problems. The call from client 2 would block until the call from client 1 returned. This example provides another good reason to stick with the default synchronization setting of Required for all Visual Basic components. It also shows how sharing objects across clients can get you in trouble.



Programming Distributed Applications with COM+ and Microsoft Visual Basic 6.0
Programming Distributed Applications with Com and Microsoft Visual Basic 6.0 (Programming/Visual Basic)
ISBN: 1572319615
EAN: 2147483647
Year: 2000
Pages: 70
Authors: Ted Pattison

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net