December 1999
Ted Pattison is an instructor and researcher at DevelopMentor
(http://www.develop.com), where he manages the Visual Basic curriculum. Ted is the author of Programming Distributed Applications with COM and Visual Basic 6.0 (Microsoft Press, 1998). |
There's one mantra that we have heard consistently since the emergence of Microsoft® Transaction Services (MTS): always call SetComplete at the end of any method in a middle-tier object. However, I believe you should only call SetComplete when it truly makes sense. A call to SetComplete tells the system that you'd like to deactivate your object when the current method call returns. This transparent deactivation of MTS/COM+ objects has been called stateless programming. In the short history of MTS, there has been a good deal of confusion about why statelessness is an essential aspect of the programming model. Some books and articles have even gone so far as to suggest that stateless programming is about reclaiming memory in the middle tier. They argue that destroying objects and reclaiming memory results in higher levels of scalability due to more efficient resource usage. This argument is both confusing and inaccurate. It's time to set the facts straight. As it turns out, there are two compelling arguments for calling SetComplete. One argument has to do with controlling the outcome of a transaction, the other with object pooling. If your component isn't involved in a transaction and doesn't support object pooling, a call to SetComplete has either no effect or a negative effect. Blindly following the SetComplete mantra without understanding the issues at hand isn't a very sound programming practice. Writing MTS Transactions
MTS provides two important transactional methods: SetComplete and SetAbort. These methods are critical for controlling the outcome of an MTS transaction. While it's important to know the specifics of when and how to call those methods inside the scope of an MTS transaction, I'm not going to discuss those details in this column (see my article in the October 1999 issue of MSJ for details). Instead, I'll focus on what happens at the end of a transaction. Whether a transaction is committed or rolled back, every object involved will be deactivated when the transaction is released. |
Figure 1 Object Creation in an MTS Transaction |
So why is it important for MTS to deactivate all the objects inside a transaction? It ensures the proper semantics of the transaction. The idea is that an object in an MTS transaction can see a data item in a consistent state only while the resource manager is holding a lock. If an MTS object were to hold a copy of a data item in the middle tier after the lock has been released, another transaction could modify the original data item inside the resource manager. The original data item and the copy would thus get out of sync and violate the ACID rules of a transaction. MTS requires the deactivation of transactional objects so that any copy of a data item must be thrown away when the resource manager releases its locks. MTS requires you to release every transactional object so you don't compromise the consistency of your system. A call to SetComplete simply forces the end of the transaction and the inevitable deactivation of your objects. SetComplete and Object Pooling
You know that a call to either SetComplete or SetAbort informs the interception layer that you'd like to deactivate your object when the current method call returns. In MTS 2.0 and earlier, objects were always destroyed by the system immediately after deactivation. Starting with Windows® 2000, COM+ objects can participate in object pooling. This means that an object can be recycled after it's been deactivated by the system. Object pooling puts a new spin on why you might like to call SetComplete. A call to SetComplete can speed up deactivation and return your object to a shared pool in a more timely manner. |
Figure 2 Object Pooling Settings |
Figure 2 shows the Activation tab in the component properties dialog in the Component Services Explorer. This dialog allows you to configure the object pooling settings for a component in a COM+ application. Notice that the Minimum pool size has been set to 5. Once you've configured your component this way, the COM+ runtime will automatically create and initialize five objects when the application is launched. The initialization time for these objects is then amortized over the lifetime of the application. What is the primary advantage of object pooling? Each client doesn't have to wait five seconds while an object in the middle tier establishes a connection. Each client simply acquires an object from the pool with an established connection. In a scenario like this, object pooling can significantly improve the throughput and increase the scalability of a COM+ application. In addition to saving processing cycles that would be associated with object creation and initialization, object pooling can also be used to provide object throttling. Throttling is a way to limit the number of clients who are utilizing a set of objects and their associated resources. For instance, you might want to limit the maximum number of outstanding connections to the mainframe application at any one time. Configuring the component to be throttled is an easy solution to this problem. The maximum pool size restricts how many users are able to use the component at any one time. If you set the maximum pool size to 10, only 10 clients can use the component at a time. Once 10 clients have activated objects, any other clients will block until one of the other clients returns an object to the pool. A blocked client will simply wait for the next available object. As long as these pooled objects call SetComplete, they are returned to the pool whenever a method returns to a client. Each client acquires, uses, and releases an object on a per-method call basis. In this example, it means that only 10 clients can be running method calls at the same time. If these objects don't call SetComplete, the maximum size indicates how many clients can create connections to an object. As you can see, calling SetComplete gives you throttling while providing higher levels of concurrency. In addition to a maximum pool size, a component also has a configurable creation timeout. A client will only block for the duration of the creation timeout. If a client times out while attempting to activate an object from the pool, it will experience a runtime error. You must then deal with this by writing an error handler in the code that will attempt to activate the object. Now let me summarize what object pooling is all about: reusing objects in which initialization is both expensive and generic. Besides my example of a component that must establish a connection to a mainframe application, there are many other situations where you might encounter components that meet this criteria. Object pooling and calls to SetComplete will increase scalability in this type of component. However, it's important to remember that a component will not really benefit from object pooling if it doesn't meet these criteria. How Visual Basic Fits In
A COM+ component must meet a fairly rigid set of requirements to support object pooling. Unfortunately, Visual Basic® 6.0 can only create components that are apartment threaded. Furthermore, all Visual Basic-based objects exhibit thread affinity due to the usage of thread local storage in Visual Basic. These threading limitations, along with a few other shortcomings, prevent Visual Basic-based objects from participating in the object pooling scheme of COM+. When you install a Visual Basic component into a COM+ application, the object pooling settings will always be disabled; there's nothing you can do to make COM+ pool your objects. Using C++ along with the ActiveX® Template Library is the easiest and most straightforward way to create COM+ components that support pooling. |
Figure 3 Connection Between an MTS Object and a Client App |
Some argue that calling SetComplete is beneficial because it aggressively reclaims memory in the middle tier. Figure 3 shows the required pieces in a connection between an MTS object and a client application running across the network. When you call SetComplete and destroy an MTS object, you are not releasing either the server-side stub or the context wrapper. These two pieces add up to about 1KB of memory. (Take a look at Don Box's column in the March 1998 issue of MSJ for more details on this figure.) In many cases the memory for the object you're destroying will be much smaller than the ongoing memory requirements for the stub and the context wrapper. For example, if you call SetComplete on a Visual Basic-based object that's 250 bytes, you're only reclaiming 20 percent of the memory that the client held in the first place. If you want to reclaim all the memory, you must destroy the stub and context wrapper by releasing the reference from the client application. However, calling SetComplete does improve thread management, and this may improve your application's performance or concurrency. MTS allocates single-threaded apartment (STA) threads to clients by associating the context wrapper with an MTS activity. Since this association is broken when SetComplete is called and the objects are deactivated, the threads servicing the clients can be managed more efficiently than by locking a thread to a user object's STA in a stateful design. Creating Stateful Components
There are a few situations where stateful components make sense. Let's look at a common example. Assume you have an MTS application where base clients are creating objects from across the LAN. In this scenario, stateful components may have some advantages over a stateless one. |
Have a burning issue to resolve in Visual Basic? Send email to Ted at tedp@sublimnl.com. |