Life Cycle Management

Team-Fly    

 
.NET and COM Interoperability Handbook, The
By Alan Gordon
Table of Contents
Chapter Two.  Comparing COM and .NET

Life Cycle Management

In order to use an object, whether it's an instance of a .NET class or a COM class, the server for that object must be loaded into memory. In the case of COM, the COM runtime must load either an in-process server or an out-of-process server that contains the COM class into memory. In the case of .NET, the CLR must locate and load the assembly that contains the class. This is true in both the intra machine and inter machine (Internet) scenarios. After the CLR loads the assembly and creates an instance of the class, the class will likely acquire other resources on the system. The class instance (object) may open a file, allocate memory for a large string, or create a network or database connection.

If you created objects and never made any attempt to destroy objects that were no longer needed and hence reclaim the resources (memory, file handles, and so forth) that they use, eventually your software would consume all of its host's memory until the machine crashed. A software bus must provide a way for developers to keep alive only those objects that they currently need or may need in the future. If you no longer need an object, you should have some way of indicating this, and there should be a method for destroying the object, thereby reclaiming the resources used by the object. Therefore, the life cycle problem really distills down to three simple questions:

  • How do you indicate that you are using an object or will use it in the future?

  • How do you specify that an object is no longer needed?

  • How do you reclaim the resources used by the object?

The COM Life Cycle Management Solution

COM's answer to these life cycle management questions is reference counting. Reference counting is a programming idiom that is fairly simple to understand. You associate an integer reference count with the resource that you want to manage. You increment this count any time that you assign some reference (usually a pointer) to the resource. When you decide that you will no longer access the resource through the reference or the reference goes out of scope, you decrement the reference count. When the reference count reaches zero, you can safely delete the resource because it can no longer be accessed by any references.

With COM, reference counting is done through the IUnknown interface. All COM objects are required to support this interface. Indeed, support for the IUnknown is part of the definition of a COM object. You can think of the IUnknown interface as part of the plug you have to build for your software components so that they will work with the COM software bus. The IUnknown interface supports two methods AddRef and Releasethat together implement reference counting. AddRef increments the reference count; Release decrements the reference count.

The COM specification stipulates that the Release method should delete an object when the reference count is zero, leading to the somewhat confusing (but legal) canonical implementation of the Release method in C++ that follows :

 STDMETHODIMP_(ULONG) CSpellCheckImpl::Release(void) {     if (0L!=--m_ref)         return m_ref;     delete this;     return 0; } 

Notice the call to delete "this" if the reference count is zero.

Unfortunately , there are a number of problems with reference counting and the IUnknown interface. It was actually fairly simple to implement the IUnknown interface, but not everyone even got this right. The following C++ code shows a common mistake:

 STDMETHODIMP_(ULONG) CSpellCheckImpl::Release(void) {     if (0L!=--m_ref)         return m_ref;     delete this;     return m_ref; } 

Did you spot the problem? If m_ref is a member variable of this implementation class, calling delete "this" will free the memory occupied by this object. The value of m_ref is therefore undefined.

The example that I just gave is only part of the problem. The biggest problem with reference counting is that youthe developermust determine when to call Release. You must also be sure not to use an interface pointer after you have called Release on it because the Release method may delete the object if the reference count is zero. If you attempt to use an interface pointer after an object has deleted itself, you would get unpredictable results and most likely a General Protection Fault (GPF). You also have to be careful that you balance each call to AddRef with a corresponding call to Release. Not doing this will cause a resource leak. These unreleased resources will pile up in memory, degrading system performance and eventually causing the system to crash. Although it would seem simple to balance each call to AddRef with a call to Release, in practice it's actually quite difficult. Part of the problem is that not all calls to AddRef are explicit. If you acquire a COM interface pointer through a method call like CoCreateInstance, the method is responsible for calling AddRef. The client application that receives the interface pointer only needs to call Release when it is done with the interface pointer. There was another more insidious problem that could appear even if you had matched every call to AddRef with a corresponding call to Release: the so-called circular reference problem. This problem may occur if you have one object (Object A) that refers to another object (Object B) and Object B in turn has a reference back to object as shown in Figure 2-6.

Figure 2-6. The circular reference problem.

graphics/02fig06.gif

The problem is that, in a scenario like this, you will probably implement the Release method in Object A so that it will release its reference to Object B when Object A's reference count goes to zero. You will probably also implement the Release method in Object B so that it will release its reference to Object A when Object B's reference count goes to zero. Therefore, Object A will not be deleted until Object B is deleted, and Object B will not be deleted until Object A is deleted. Both objects will wind up stuck in memory until you shut down the process.

Note

The Visual Basic Concepts guide (in Visual Studio 6) has an excellent explanation of circular references and how to solve them. Find it on the web at the following URL: msdn.microsoft.com/library/devprods/vs6/vbasic/vbcon98/vbcondealingwithcircularreferences.htm.


No wonder that even Microsoft couldn't seem to use COM reference counting correctly. In the course of my own work on a recent project, we noticed a fast memory leak on the server portion of our application when we first ported it from Windows NT to Windows 2000. After several days of investigating, we determined that the problem was in the cross-context marshaling code in COM+. The problem was caused (in Microsoft's own words) by "improper release of objects passed as parameters between VB objects in the same apartment but in different COM+ contexts." Microsoft fixed this problem in Service Pack 1 of Windows 2000.

Note

See article ID Q252934 in the Microsoft knowledgebase at support.microsoft.com/support/kb/articles/Q252/9/34.asp for more information about this memory leak problem.


We also encountered another problem that caused us to abandon using drag-and-drop with Windowless ActiveX controls. In the words of a Microsoft support engineer, the problem was "either VB or IE is calling Release on an interface pointer one too many times."

Even though I previously wrote a book extolling the virtues of COM and COM+, I must admit that the COM solution to life cycle management (reference counting) was simply too error prone. Thankfully life cycle management is much simpler in .NET

The .NET Life Cycle Management Solution

The problems with the COM reference counting can be distilled down to three main problems:

  • Developers releasing objects that are still in use. This will likely lead to a GPF (a crash) if the call to Release caused the object to delete itself.

  • Developers neglecting to call Release on an object when it is no longer needed. This will cause a memory (or other resource) leak.

  • Developers misusing the reference counting functions (AddRef and Release) in other ways, that is, calling Release on an interface pointer too many times or not enough times.

The solution to all of these problems is to let the runtime environment determine when an object cannot be used any more. After the system determines that an object cannot be used, it can just go ahead and destroy it. The algorithms used to determine when an object is no longer in use and the mechanisms used to delete these objects is a well-studied topic in academic circles, and it is called Garbage Collection (GC).

A Garbage Collecting environment is the best of all possible worlds for developers. There is no need for special methods like AddRef and Release to control the life cycle of objects. Developers no longer have to track the life cycle of their objects. You simply instantiate objects as you need them using the new operator, and you do not have to concern yourself with when the object will be destroyed and its resources reclaimed. When your program can no longer access an object, the system will destroy it automatically. Think about it. No more memory leaks, and no more dangling references, which is what happens when you attempt to use an object that you have already deleted.

I explore how .NET GC works in Chapter 3 when I do the drilldown on the CLR. For now, a simple explanation is that, for an object to be accessible by a running instance of a program, a reference to the object must be stored in one of the following locations, or it must be in a tree of references that originates in one of these locations:

  • A global or static variable

  • A local or parameter variable on the stack

  • A Central Processing Unit (CPU) register

These locations are called the application's roots. When the CLR senses that memory is low, it walks through every single object that a program has allocated starting at the roots. The CLR builds a graph of all the objects. Any objects not in the graph are no longer accessible by the program and can be deleted. Of course, the algorithm is far more complicated than I mention here, but this is enough for you to get the gist of how it works until I talk about it in more depth in Chapter 3.

Before the CLR deletes the object, it checks if the object contains a special method called a Finalize Destructor. If it does, the CLR will call the object's Finalize Destructor prior to reclaiming the object's memory. The Finalize Destructor is your opportunity to do any cleanup required by your object. This cleanup may include everything from closing a file to removing a list of items from a collection. It's tempting to think that the Finalize Destructor is the same as a C++ destructor, but it's not. A destructor is called in a deterministic manner. In other words, given a piece of code, it is fairly easy to determine when the destructor for an object will be called. If a C++ class contains a destructor, the destructor will be called for stack variables when the variable goes out of scope. The destructor will be called for heap variables ( variables created with the new operator) only when you explicitly call the C++ delete operator.

The Finalize Destructor on an object will be called when (and if) the GC runs. The time when this occurs is nondeterministic. In many cases, it will not occur until the application closes down. Therefore, if your objects are holding a resource that is scarce or expensive to hold on to, such as a file handle, network, or database connection, you should implement a method that a client can call to free that resource explicitly. Relying on the Finalize Destructor will cause the resource to be held much longer than is otherwise necessary. By convention, this method should be called Close if it is possible to reopen the resource using the same object instance. This might be true of a file, for instance. The method should be called Dispose if the object should no longer be used after the method has been called. The preferred way to add a Dispose method to your object is to implement the IDisposable interface, which contains a Dispose method. There is no IClose interface, unfortunately.

Life Cycle Management on Remote Objects

So far I have discussed .NET life cycle management in the intra machine case. What about life cycle management when you are working with remote objects either on your LAN or across the Internet? In a distributed environment, the client communicates with a remote object on the server through a proxy object that runs on the client machine. This proxy appears to the client to be the remote object, but, in reality, the proxy turns requests on the proxy into a message that can be sent across the network to the actual remote object. System software on the server machine will unpack the message, call the requested method on the remote object and then send the return value and any output parameters back to the proxy object.

In a distributed environment, the client proxy and the server object reside on different machines and thus use different instances of the CLR. Therefore, their life cycles must be managed separately. Even if there are no longer any object pointers referencing the proxy object on the client, that doesn't automatically mean that the server object will be garbage collected. Ideally, when the proxy is garbage collected on the client, it should send a message to the remote object indicating to the CLR on the server machine that it may release its reference to the remote object. This should eventually cause the remote object to be garbage collected also. Unfortunately, this approach is not sufficient. Imagine what would happen if the client application crashed before the client could send a message to the server that the remote object was no longer needed. The remote object would continue to "live" on the server even though the client was no longer using it.

DCOM handled this problem by having each client ping its remote objects every 2 minutes. The server would delete an object if it did not receive a ping from any of its clients for 3 ping periods (6 minutes). Unfortunately, if there were enough clients and objects, this pinging could generate significant network traffic. Therefore, COM performed this pinging intelligently, grouping all the pings for a single machine into one message and sending only changes (deltas) to this group .

.NET remoting uses leased-based life cycle management. The .NET remoting infra-structure contains a Leasing Distributed Garbage Collector (LDGC), which associates a lease with each remote object. A lease is an object that implements the System.Runtime.Remoting.Lifetime.ILease interface, which has the properties shown in Table 2-1.

Table 2-1. The members of the ILease interface

Property

Description

CurrentLeaseTime

A read-only property that contains the amount of time remaining on the lease.

InitialLeaseTime

A read/write property that contains the amount of time initially assigned to the lease (default = 5 minutes)

RenewOnCallTime

A read/write property that contains the amount of time to add to the current lease time after each call (default = 2 minutes)

SponsorshipTimeout

A read/write property that contains the amount of time to wait for a sponsor to renew the lease (default = 2 minutes)

CurrentState

A read-only property that contains the current status of the lease. A lease can have the following states: Active, Expired , Initial, Renewing, Null. You can only set the properties of a lease when it is in the Initial state.

Register

A method that you can call to register a sponsor for the lease.

Unregister

A method that you can call to unregister one of a lease's sponsors.

Renew

A method that you can call to renew a lease.

You can access the lease for a remote object by calling the GetLifetimeService method on the object as shown below.

 Company obj=new Company(); ILease lease=(ILease)obj.GetLifetimeService(); 

If you execute the code shown above from a client you will not be able to assign to the properties of the lease because the lease will already be active and you can only update the properties of a lease when it is in the initial state. You will see shortly how to set these properties.

Note

The GetLifetimeService method is a member of the System.MarshalByRefObject class. An object that will be called from a remote AppDomain must inherit from this class either directly or indirectly.


Each lease starts out with a default, initial time of 5 minutes. All leases in an AppDomain (see the sidebar in this chapter to understand what an AppDomains is) are controlled by a lease manager as shown in Figure 2-7.

Figure 2-7. Lease-based lifetime management.

graphics/02fig07.gif

The lease manager periodically checks for expired leases in its AppDomain. The time period between these checks is determined by the LeaseManagerPollTime property on the System.Runtime.Remoting.Lifetime.LifeTimeServices class. The default polling time is 10 seconds. When an object's lease has expired, the lease manager, will remove all references to the object, thereby allowing the remote object to be garbage collected.A lease can be renewed 3 ways:

  1. A lease is renewed whenever a client makes a call on the object. The lease is renewed for the amount of time specified by the RenewOnCallTime property. Specifically, the amount of time remaining on the lease is set to the Max(remaining time on the lease,RenewOnCallTime).

  2. A lease can also be renewed by explicitly calling the Renew method on the ILease interface and passing in a TimeSpan object. A client can obtain an ILease interface by calling the GetLifetimeService method on the remote object.

  3. You can also renew a lease by registering a sponsor. A sponsor is an object that implements the System.Runtime.Remoting.Lifetime.ISponsor interface. When an object's lease expires , the lease manager will contact all of the sponsors registered for the object and give them an opportunity to renew the lease. The lease manager will wait for the length of time specified by ILease.SponsorshipTimeout before marking the object for garbage collection. A client can register a sponsor by calling the GetLifetimeService method to retrieve an ILease interface and then calling the Register method passing in an object that implements the ISponsor interface.

A remote object can configure its leasing- related parameters in one of two ways: programmatically or with a configuration file. To set leasing parameters programmatically override the MarshalByRefObject.InitializeLifetimeService method on your remote object as shown here.

 public class Company : MarshalByRefObject {  // code omitted for clarity... public override object InitializeLifetimeService() {          ILease myLease=(ILease) base.InitializeLifetimeService();          if (myLease.CurrentState == LeaseState.Initial)          { myLease.InitialLeaseTime= TimeSpan.FromSeconds(30);           myLease.RenewOnCallTime= TimeSpan.FromMilliseconds(100);           myLease.SponsorshipTimeout= TimeSpan.FromMilliseconds(100);         }         return myLease; } // code omitted for clarity... } 

or you can create a configuration file as shown here.

 <configuration>       <system.runtime.remoting>         <application>           <lifetime leaseTime="30S" renewOnCallTime="100MS" sponsorshipTimeOut="100MS" leaseManagerPollTime="100MS" />  // the rest of this config. file is omitted for clarity...         </application>       </system.runtime.remoting> </configuration> 

You can then read this configuration file from your remote host executable as shown here.

 RemotingConfiguration.Configure("configfilename"); 

Or if you are using IIS as your host executable, you can simply add the "lifetime" element shown above to your web.config file.

If lease-based life cycle management sounds complicated, keep in mind that, in most cases, you won't have to concern yourself with it. The CLR and the .NET remoting infrastructure provide reasonable defaults for most parameters so there is no need to change anything. Moreover, in many cases when you are accessing .NET objects remotely, you will do so using Web services or Single Call objects. When you configure your remote object to be a single call object, the remoting or ASP.NET runtime will instantiate an object to service the request, call the desired method on the object, and then destroy the object. When you configure your remote object's to be singlecall objects, the runtime does not use lease-based lifecycle management.

Note

I talk more about .NET remoting, which is the .NET name for accessing objects across process, machine, and AppDomain boundaries in Chapter 11. In that chapter, you will also learn more about lease-based life cycle management, the MarshalByRefObject class, configuration files and client versus server activation, well-known objects, and single-call versus singleton life cycles.


Understanding AppDomains

An AppDomain is an isolated sub-process within a process. A default AppDomain is created when the CLR initializes within a process and this AppDomain is not destroyed unti the process terminates. In addition a process that hosts the CLR can create additional AppDomains. The most important aspect of an AppDomain is its isolation. Code that is executing in one AppDomain cannot directly access memory or objects that reside in a different AppDomain. In order to access objects across AppDomains they must be either serialized into the calling AppDomain (marshal by value objects) or, if the type derive from the System.MarshalByRefObject class, they can be accessed across AppDomains via a proxy that resides in the calling AppDomain and forwards the request to the AppDomain where the object resides (these are called marshal by reference objects).


Team-Fly    
Top
 


. Net and COM Interoperability Handbook
The .NET and COM Interoperability Handbook (Integrated .Net)
ISBN: 013046130X
EAN: 2147483647
Year: 2002
Pages: 119
Authors: Alan Gordon

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