Weak Reference

Weak Reference

Weak references are one of my favorite features of the .NET Framework. A weak reference accords an object less persistence than a strong reference. A strong reference is a conventional reference and created with the new operator. As long as there is a strong reference pointing to the object, the object persists in memory. If not, the object becomes a candidate for removal and is collected in a future garbage collection. Conversely, a weak reference to an object is insufficient to retain that object in memory. A weak reference can be collected as memory is needed. You must confirm that weakly referenced objects have not been collected before use.

Weak references are not ideal for objects that contain information that is expensive to rehydrate. Information read from a persistent source such as a file or data store is preferred. You can simply reread the file or request the dataset again. Rehydrating a dataset can be light or heavy based on several factors, including the location of the dataset.

The central type for establishing weak references is, appropriately, the WeakReference type. These are the steps for creating and using a weak reference:

A weak reference usually starts life as a strong reference:

XNames objTemp = new XNames();

Create a WeakReference type. In the constructor, initialize the weak reference with the strong reference. Afterward, set the strong reference to null. An outstanding strong reference prevents the weak reference from controlling the lifetime of the object:

XNames objTemp = new XNames();
weaknames=new WeakReference(objTemp);
objTemp = null;

Before using the object, request a strong reference to the object from the weak reference. WeakReference.Target returns a strong reference to the weak object. If WeakReference.Target is null, the object has been collected and is no longer in memory. In this circumstance, the object needs to be rehydrated:

if (weaknames.Target == null)
{
    // do something
}

The following class reads a list of names from a file:

class XNames
{
    public XNames()
    {
        StreamReader sr=new StreamReader("names.txt");
        string temp = sr.ReadToEnd();
        _Names = temp.Split('\n');
    }
    private string [] _Names;

    public IEnumerator<string> GetEnumerator()
    {
        foreach(string name in _Names)
        {
            yield return name;
        }
    }
}

The preceding code is from the Weak application. It uses the XNames class to display the names from a file in a list box. The Weak application is shown in Figure 14-6.

Image from book
Figure 14-6: Weak application

In the Weak application, a weak reference is created and initiated with an instance of the XNames class. The code in the Fill List button enumerates the names of the XNames instance to populate the list box. Before using the instance, the status of the instance must be confirmed. Is it collected or not? The Apply Pressure button applies memory pressure to the application that eventually forces the weakly referenced object to be collected. When this occurs, the content in the list box is removed. You must then refill the list box using the Fill List button.

This is the code from the form class of the Weak application that pertains to weak references:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }
    private void btnFill_Click(object sender, EventArgs e)
    {
        if (weaknames.Target == null)
        {
            DialogResult result=MessageBox.Show(
                "Rehydrate?",
                "Names removed from memory.",
                MessageBoxButtons.YesNo);
            if (result == DialogResult.No)
            {
                return;
            }
            else
            {
                weaknames.Target = new XNames();
            }
        }
        foreach(string name in (XNames)weaknames.Target)
        {
            lblNames.Items.Add(name);
        }
    }

    private WeakReference weaknames;

    private void Form1_Load(object sender, EventArgs e)
    {
        XNames objTemp = new XNames();
        weaknames=new WeakReference(objTemp);
        objTemp = null;
    }

    private void btnApply_Click(object sender, EventArgs e)
    {
        objs.Add(new ZClass());
        if (weaknames.Target == null)
        {
            lblNames.Items.Clear();
        }
    }

    List<ZClass> objs = new List<ZClass>();
}

internal class ZClass
{
    public long[] array = new long[7500];
}

Weak Reference Internals

Weak references are tracked in short weak reference and long weak reference tables. Weak references are not created on the managed heap, but are entries in the short weak reference or long weak reference tables. Both tables are initially empty.

Each entry in the short weak reference table is a reference to a managed object on the heap. When garbage collection occurs, objects referenced in the short weak reference table that are not strongly rooted are collectable. The related slot in the table is set to null. References to finalizable objects that are being collected are moved from the finalization to the FReachable queue.

Entries in the long weak reference table are evaluated next. Long weak references are weak references that track an object through finalization. This allows the resurrection of a weakly referenced object. Objects referenced in the long weak reference table that are not strongly rooted are collectable.

Weak Reference Class

Table 14-1 lists the important members of the WeakReference class.

Table 14-1: WeakReference Class

Member Name

Description

WeakReference(object target)

The one-argument constructor initializes the weak reference with the target object.

WeakReference(object target, bool trackResurrection)

The two-argument constructor initializes the weak reference with the target object. If trackResurrection is true, the object is also tracked through finalization.

IsAlive

This is a property and gets whether or not the target object has been collected.

Target

This is an object property and gets or sets the object being referenced.

Critical Finalization Objects

Conditions can prevent a finalizer from running, and critical cleanup code might not execute. Resource leakage and other problems can occur when critical finalization code is abandoned. .NET Framework 2.0 introduces critical finalizable objects to ensure that critical finalizers run. The CLR is more diligent about executing finalizers in critical finalizer objects. Conditions that can prevent a normal finalizer from running, such as a forcible thread abort or an unload of the application domain, do not affect a critical finalizer. This is especially an issue in environments that host the CLR, such as Microsoft SQL Server 2005. A CLR host can asynchronously interrupt a managed application, which can strand important finalizers.

During garbage collection, critical finalizers run after the normal finalizers.

Critical finalizer objects are derived from the CriticalFinalizerObject, which is located in the System.Runtime namespace. The finalizer runs uninterrupted in a Constrained Execution Region (CER).

Constrained Execution Region

CER is a region of reliable code that is guaranteed to run. Even an asynchronous exception will not prevent the region from executing to completion. Within the CER, developers are constrained to certain actions. This is a shortlist of some of the actions that should be avoided in a CER:

  • Boxing

  • Unsafe code

  • Locks

  • Serialization

  • Calling unreliable code

  • Actions likely to cause an asynchronous exception

In a CER, the CLR delays an asynchronous exception until the region is exited. The CLR performs a variety of checks and does some preparation to ensure that code in the CER runs uninterrupted.

The RuntimeHelpers.PrepareConstrainedRegions static method places a subsequent catch, finally, or fault handler in a constrained region. The RuntimeHelpers class is in the System .Runtime.CompilerServices namespace. The PrepareConstrainRegions method call should immediately precede the try statement. The code in the try block is not reliable and can be interrupted. However, the related handler is within the CER and is uninterruptible.

Developers must make reliability assurances for code in a constrained region. The CLR does not strictly enforce the reliability constraints within the CER. Instead, the CLR relies on developer commitment, which is a reliability contract. Reliability contracts state the level of compliance for a method, class, or assembly. Set a reliability contract with the ReliabilityContractAttribute. This attribute is found in the System.Runtime.ConstrainedExecution namespace. The Reliability-ContractAttribute constructor has two parameters. The first parameter is the ConsistencyGuarantee property, which indicates the potential scope of corruption if an asynchronous exception occurs during execution. For example, Consistency.MayCorruptAppDomain means that an asynchronous exception can leave the application domain in an unreliable state. The second parameter is the Cer property, which is a completion guarantee for code in a CER region. Cer.Success is the highest guarantee and promises that the code will successfully complete when running in a CER, assuming valid input.

Here is an example of a CER region:

[ReliabilityContract(Consistency.WillNotCorruptState,
        Cer.Success)]
class ZClass {

    void MethodA() {
        RuntimeHelpers.PrepareConstrainedRegions();
        try {
        }
        finally {
            // CER Region
        }
    }

}

Stephen Toub authored a detailed and informative article on CERs called "Keep Your Code Running with the Reliability Features of the .NET Framework." It can be found at the following link: http://msdn.microsoft.com/msdnmag/issues/05/10/reliability/default.aspx

Safe Handles

As a convenience, partial implementation of the CriticalFinalizerObject type is found in the System.Runtime.InteropServices namespace. The implementation creates a critical finalizer, which contains a CER. CriticalHandle, SafeHandle, SafeHandleMinusOneIsInvalid, and Safe-HandleZeroOrMinusOneIsInvalid are derived from CriticalFinalizerObject and are adequate for most purposes. These classes are found in the Microsoft.Win32.SafeHandles namespace. Safe-Handle implements reference counting, whereas CriticalHandle does not support reference counting. Both classes are essentially safe wrappers for kernel handles and ensure that kernel handles are properly closed in the future. The classes are abstract and require minimal implementation in the derived type. The .NET Framework class library (FCL) provides a complete implementation of the CriticalHandle and SafeHandle classes in the Microsoft.Win32.SafeHandles namespace. SafeWaitHandle is one implementation. It is a wrapper for a wait handle and indirectly extends the SafeHandle class.

In the following code, the PipeHandle class is a safe wrapper for a pipe handle. It also exposes the CreatePipe and the CloseHandle application programming interfaces (APIs) using interoperability. Because the method is called in a CER, a reliability contract is placed on the CloseHandle method. PipeHandle derives from the SafeHandleMinusOneIsInvalid class for the behavior of a critical finalizer object. The finalizer of the base class automatically calls the ReleaseHandle method. The ReleaseHandle of the derived type, PipeHandle, also has a reliability contract.

In the AnonymousPipe constructor, two PipeHandle instances are initialized, which are a read and write handle. The handle values are also displayed. The PipeHandle and AnonymousPipe classes are included in the Pipe application found on the companion CD-ROM. In the sample code, the application domain hosting the PipeHandle instances can be forcibly unloaded. Because the instances are critical finalizer objects, their destructors are called despite the unloading of the application domain:

public sealed class PipeHandle :
    SafeHandleMinusOneIsInvalid
{

    private PipeHandle()
        : base(true)
    {

    }

    [ReliabilityContract(Consistency.WillNotCorruptState,
            Cer.Success)]
    protected override bool ReleaseHandle()
    {
        return CloseHandle(handle);
    }

    [DllImport("kernel32.dll")]
    extern public static bool CreatePipe(
        out PipeHandle hReadPipe,
        out PipeHandle hWritePipe,
        IntPtr securityAttributes,
        int nSize);

    [ReliabilityContract(Consistency.WillNotCorruptState,
           Cer.Success)]
    [DllImport("kernel32.dll")]
    public static extern bool CloseHandle(IntPtr handle);

}

public class AnonymousPipe
{

    public AnonymousPipe()
    {

        PipeHandle.CreatePipe(out readHandle, out writeHandle,
            IntPtr.Zero, 10);
        MessageBox.Show((readHandle.DangerousGetHandle())
            .ToInt32().ToString());

        MessageBox.Show((writeHandle.DangerousGetHandle())
            .ToInt32().ToString());
    }

    private PipeHandle readHandle = null;
    private PipeHandle writeHandle = null;
}

Managing Unmanaged Resources

Managed code often relies on unmanaged resources. The unmanaged resource is typically accessible through a managed wrapper. The MyDevice program is an unmanaged application that emulates a hardware device. DeviceWrapper is a managed wrapper for the MyDevice unmanaged resource. This is the code for the DeviceWrapper class:

public sealed class MyDevice
{
    static private int count = 0;

    public MyDevice()
    {
        obj = new MyDeviceLib.DeviceClass();
        ++count;
    }

    private MyDeviceLib.DeviceClass obj;

    public void Open()
    {
        obj.OpenDevice();
    }

    public void Close()
    {
        obj.CloseDevice();
    }

    public void Start()
    {
        obj.StartCommunicating();
    }

    public void Stop()
    {
        obj.StopCommunicating();
    }

    ~MyDevice()
    {
        // resource released
        --count;
    }
}

Memory Pressure

The wrapper for an unhandled resource can hide the true memory cost of an object. Incorrect accounting of unhandled memory in the managed environment can cause unexpected out of memory exceptions.

.NET 2.0 introduces memory pressure, which accounts for unmanaged memory in the managed environment. This prevents a wrapper to an unmanaged resource from hiding an elephant in the closet. Memory pressure forces garbage collection sooner, which collects unused instances of the wrapper class. The wrapper releases the unmanaged resource to reduce the memory pressure on both managed and unmanaged memory.

The GC.AddMemoryPressure method adds artificial memory pressure on the managed heap for an unmanaged resource, whereas the GC.RemoveMemoryPressure method removes memory pressure. Both methods should be integrated into the wrapper class of the unmanaged resource. Call AddMemoryPressure and RemoveMemoryPressure in the constructor and Dispose method, respectively. Each instance of the MyDevice unmanaged resource allocates 40,000 bytes of memory on the unmanaged heap. In the following code, the constructor and destructor for the MyDevice wrapper now account for the unmanaged memory:

public MyDevice()
{
    GC.AddMemoryPressure(40000);
    obj = new MyDeviceLib.DeviceClass();
    ++count;
}

~MyDevice()
{
    GC.RemoveMemoryPressure(40000);
    // resource released
    --count;
}

Handles

Some native resources are available in limited quantities. Exhausting the resource can hang the application, crash the environment, or cause other adverse reactions. The availability of a limited resource should be tracked. When the resource is exhausted, corrective action should occur. Some limited kernel resources, such as a window, are assigned handles. The HandleCollector class is introduced in .NET Framework 2.0 to manage handles to limited kernel resources. Despite the name, the HandleCollector class is not limited to tracking kernel handles. You can use the HandleCollector class to manage any resource that has limited availability. The HandleCollector class is found in the System.Runtime.InteropServices namespace.

The HandleCollector class has a three-argument constructor that configures the important properties of the type. The arguments are the name: initial threshold and maximum threshold. The initial threshold sets the minimal requirement for starting garbage collection. The maximum threshold sets the limit where garbage collection is forced. When the availability is exceeded, garbage collection is triggered. Hopefully, this will collect managed wrappers that are holding unmanaged resources and replenish native resources. Using the constructor, create a static instance of the HandleCollector in the managed wrapper of the unmanaged resource. In the constructor, call HandleCollector.Add. In the finalizer, call HandleCollector.Remove.

The following code shows the MyDevice class revised for the HandleCollector class. The MyDevice unmanaged resource supports an initial threshold of three simultaneous connections and a maximum of five. You can test the effectiveness of the wrapper in the UseResource application by clicking the Connect button and monitoring the message boxes.

public sealed class MyDevice
{
    static private HandleCollector track=
        new HandleCollector("devices", 3,5);

    static private int count = 0;

    public MyDevice()
    {
        GC.AddMemoryPressure(40000);
        track.Add();
        obj = new MyDeviceLib.DeviceClass();
        ++count;
        MessageBox.Show("Device count: " + count.ToString());
    }

    private MyDeviceLib.DeviceClass obj;
    public void Open()
    {
        obj.OpenDevice();
    }

    public void Close()
    {
        obj.CloseDevice();
    }

    public void Start()
    {
        obj.StartCommunicating();
    }

    public void Stop()
    {
        obj.StopCommunicating();
    }

    ~MyDevice()
    {
        GC.RemoveMemoryPressure(40000);

        track.Remove();
        // resource released
        --count;
    }
}