This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.
|
|
Ted Pattison |
Creating Objects Properly in an MTS App |
Let's begin by looking at a Visual Basic project that doesn't reference either the MTS or the COM+ type library. In this kind of project you must choose between the New operator and the CreateObject function to create a COM object. An example of a project that doesn't reference one of these type libraries is a desktop client application that's going to be activating MTS objects from across the network. A client application that runs outside of the MTS/COM+ runtime is known as a base client. When a base client creates an MTS/COM+ object using either the New operator or the CreateObject function, it results in standard activation through the COM Service Control Manager (SCM). The New operator is a little bit faster than the CreateObject function because the client application doesn't need to resolve a ProgID to a CLSID at runtime. Using the New operator also provides a compile-time type check on the name of the class you're instantiating. The Visual Basic compiler cannot verify whether a ProgID is valid at compile time when you use the CreateObject function. This can make your code somewhat harder to debug. On the other hand, the CreateObject function can offer a little more flexibility than the New operator because you don't have to commit to a class name at compile time. You can devise a design where the client code dynamically chooses a ProgID from a set of compatible components at runtime. Each technique has an advantage or two over the other, but both work just fine. However, once you start running your components inside the MTS runtime, things get more complicated. Using the New operator or the CreateObject function will often get you into trouble because of the concurrency model and interception scheme built into MTS. For now, let's concentrate on the Windows NT 4.0 version of MTS prior to the release of COM+ and Windows® 2000. Activities
Concurrency in MTS is based on activities. An activity is a logical thread of execution created by the system on behalf of a single base client. When a base client activates an MTS object, the system transparently creates a new activity, as shown in Figure 1. Every single MTS object is created inside the context of an activity. Once an object is created inside a specific activity, it must spend its entire lifetime there. |
Figure 1: An MTS Activity |
The MTS programming model dictates that each base client should get its own activity. This means that an MTS application should manage one logical thread for each base client. Furthermore, every object belonging to the same base client should run in only one activity. Activities are a valuable abstraction because programmers can think in terms of logical threads while the system worries about the details of the physical threads. When you follow the rules as they're laid out by the MTS programming model, the system is able and willing to conduct thread pooling for you behind the scenes.
When a base client creates an object from a component that's been registered in an MTS package, the MTS runtime creates a new activity. Since this new activity only represents a logical thread, MTS must supply a physical thread in order for the object to really do anything. The MTS runtime binds each new activity to an apartment thread at creation time. In MTS 2.0, the system maintains a thread pool on a per-process basis that can hold up to 100 apartment threads. The system is able to supply each new activity with its own apartment thread until the thread pool reaches capacity. When the number of activities exceeds the number of apartment threads, the system begins to assign multiple activities to individual apartment threads. When two activities map to the same apartment thread, the code executing on behalf of one user can potentially block the call of another user. However, this should not concern you as an MTS programmer. You're supposed to think in terms of activities and let the MTS runtime manage the physical threads as it sees fit. The designers of MTS have tried to make this threading/concurrency model as simple as possible. The abstraction of the apartment thread in COM relieves you from worrying about synchronization. The abstraction of the activity in MTS builds upon the apartment thread by also relieving you from worrying about the complexities of thread pooling. You need only follow a handful of rules to make the most of the MTS concurrency model. Creating Objects Let's review the rules you should keep in mind when you're writing an MTS application. Each activity should represent a set of objects that belong to a single base client. From this rule, two other rules emerge. First, no two clients should ever be connected to the same activity and, therefore, to the same object. MTS simply doesn't accommodate designs where a singleton object is shared across multiple clients. Second, you should avoid creating objects in different activities when they belong to the same base client. The consequence of breaking this rule is that you must often write code to explicitly propagate new objects into the activity of their creator. Let's reexamine Figure 1. Once the base client has activated an MTS object, this object might create additional MTS objects inside the same server-side process. When you're writing the code for a method in an MTS component where you're creating a secondary MTS object, you must do it properly. If you don't, your new object might be created in a different activity and, therefore, run on a different physical thread. When you instantiate one MTS component from another, you should do so by sending the activation request directly to the MTS runtime. You do this by calling the CreateInstance method on the ObjectContext interface. CreateInstance takes a single parameter for the ProgID of the new object. The following code demonstrates the proper technique: |
|
As in the case of the CreateObject function, Visual Basic cannot verify the ProgID you pass to CreateInstance at compile time. This can cause some frustrating side-effects during debugging. An invalid ProgID will usually cause the Visual Basic debugger to go up in flames. Be extra careful and make sure you always pass a valid ProgID when you are calling CreateInstance.
When an object calls CreateInstance, it is telling the MTS runtime to create the requested object in the context of the current activity. The MTS runtime creates the new object and places a context wrapper between it and its creator, as shown in Figure 1. You should note two important things here. First, since the two objects are running on the same apartment thread, there is no need for a proxy/stub layer between them. Second, the context wrapper is inserted between the two objects so that the MTS interception scheme is set up properly. While you can activate MTS objects from a base client using either the New operator or the CreateObject function, you should be cautious when using these techniques in an MTS component. Using these techniques can result in several undesirable situations. Figure 2 shows the three different techniques for instantiating one MTS component from another MTS component. Figure 3 shows how the objects are laid out inside the MTS application. Assume that Object 1 is the creator of all the other objects. Object 2 has been created correctly using the CreateInstance method. The new object has been created in the activity of its creator, and the context wrapper has been set up correctly. However, there are problems with both Object 3 and Object 4. Let's start by examining the problem with Object 3 and what happens when you try to create one MTS object from another using the CreateObject function. When an MTS object calls CreateObject, the activation request bypasses the MTS runtime and is sent down to the SCM. The SCM, in turn, calls back to the MTS runtime with the same activation request. However, the MTS runtime doesn't know that the activation request is coming from inside the MTS runtime. Instead, MTS assumes that another base client is activating the object. MTS creates the new object in a new activity. This means that the new object and its creator will more than likely run on different apartment threads. The two objects are bound together across a proxy/stub layer, which significantly degrades performance and unnecessarily consumes another apartment thread from the pool. You should also note that since these two objects are running in separate activities, they cannot be part of the same MTS transactionevery MTS transaction must be scoped inside the context of a single activity. You'll also get into trouble if you try to instantiate one MTS component from another using the New operator. When you do so, there are two scenarios you must consider. In the first scenario, the creator component and the component being instantiated live in separate DLLs. In this case, a call to New is sent down to the SCM just like a call to CreateObject. This causes the new object to be activated in a new and separate activity. The result is the same as a call to CreateObject. As you have already seen, this situation is undesirable. In the second scenario, the creator component that calls the New operator and the component being instantiated are compiled into the same DLL. The problem here is more subtle, but it can lead to even greater frustration. When a Visual Basic object calls New on a class name that's compiled into the same DLL, the Visual Basic runtime creates and binds the object on its own without involving either the MTS runtime or the SCM. The new object gets loaded on the same thread as the creator, but it isn't a valid MTS object because it doesn't get its own context wrapper. This means that MTS has not set up the interception scheme properly. Look at Object 4 in Figure 3 to see what happens. Neither COM nor MTS know that a new object has been created. When Object 4 calls GetObjectContext, it's given a reference for the object context of its creator, Object 1. In this situation your code can exhibit strange and mysterious behavior. To demonstrate one potential problem you might encounter, let's say you're writing an MTS transaction using Object 1 and Object 4. If Object 1 (the root object) creates Object 4 (the secondary object) using the New operator, Object 4 will not have its own context wrapper. If Object 4 calls SetAbort, it's voting to roll back the transaction. In the MTS programming model, any object that calls SetAbort should be capable of rolling back the transaction. However, if Object 1 calls SetComplete after Object 4 calls SetAbort, the transaction will be committed. This is due to the fact that Object 4 did not get its own context wrapper. Its vote to abort the transaction was overridden by Object 1. Using New and CreateObject As it turns out, sometimes you can and should use the New operator in the method of an MTS component. You can use the New operator when instantiating components that are not configured in an MTS package. For example, you should use the New operator whenever you want to create a new ActiveX® Data Object (ADO). ADO components aren't registered with MTS. ADO objects don't need a context wrapper and they don't use the object context. From what you've seen here, an MTS application can run a mixture of MTS components and standard COM components. An object created from an MTS component requires a context wrapper; an object created from an standard COM component does not. When you instantiate a standard COM component from an MTS component using the New operator or the CreateObject function, the new object is created
in the same activity as the creator. However, a component that has been programmed against the MTS type library has a dependency on its object context. A component with such a dependency must be registered in an MTS package and properly instantiated as an MTS object. As a rule of thumb, one MTS component must always instantiate another MTS component using CreateInstance when running under Windows NT 4.0. COM+ and Windows 2000 I'd like to conclude this month's column with a brief discussion of how COM+ and Windows 2000 influence the issues I've discussed. One of the big improvements in COM+ is the integration of COM and MTS. The two programming models have been unified. This eliminates the issue of calling to the MTS runtime versus calling to the SCM. This means that a call to the CreateObject function under Windows 2000 is the same as a call to CreateInstance. As long as a component is configured properly, the new object will always be loaded into the activity of its creator. In fact, in Windows 2000 a call to CreateObject is preferred to a call to CreateInstance. CreateInstance is mainly included under the COM+ programming model for backward compatibility with MTS objects. If you're using Visual Basic 6.0 or earlier, the New operator will continue to cause problems in COM+. If the creator component calls the New operator on a component compiled into the same DLL, the Visual Basic runtime still creates and binds the object without the help of COM+. COM+ cannot properly set up its interception scheme between the two objects. For this reason, you should use the CreateObject function over the New operator in situations where the two components in question are compiled into the same DLL. |
From the August 1999 issue of Microsoft Internet Developer.
|