Weak Reference

Weak references are one of my favorite features of the .NET Framework. As you might guess from the name, a weak reference has less persistence than a strong reference, which is a conventional reference created with the new operator. So long as there is a strong reference for the object, the object persists in memory and cannot be collected. If there is no outstanding reference, the object becomes a candidate for removal 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. For this reason, you must confirm that weakly referenced objects have not been collected before you use the object.

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 data, such as local, network share, or remote.

Use the WeakReference type to create a weak reference object. These are the steps for creating and using a weak reference:

  1. A weak reference usually begins as a strong reference, so create a strong reference:

    XNames objTemp = new XNames();
  2. 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. Therefore, it is imperative to set the strong reference to null:

    XNames objTemp = new XNames();
    weaknames = new WeakReference(objTemp);
    objTemp = null;
  3. Before using the object, request a strong reference to the weakly referenced object. WeakReference.Target returns a strong reference to the weak object. If WeakReference.Target is null, the object has been garbage-collected and no longer is in memory. The object must be rehydrated. If WeakReference.Target is not null then the object has not been garbage-collected and is available for immediate use. Here is code that tests the Target property:

    if (weaknames.Target == null)
    {
        // rehydrate
    }

The following class is used in the sample code for weak references to read a list of names from a file (shown later in this section):

class XNames
{
    public XNames()
    {
        StreamReader sr = new StreamReader("names.txt");
        string temp = sr.ReadToEnd();
        _Names = temp.Split('
'),
    }
    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 17-5.

The Weak application

Figure 17-5. The Weak application

In the Weak application, a weak reference is created and initiated with an instance of the XNames class. The code for the Fill List button (the btnFill_Click method) enumerates the names of the XNames instance to populate the list box. Before using the instance, the status of the instance, which is a weak reference, must be confirmed. Has it been garbage-collected or not? If it has been garbage-collected, the user is prompted whether to rehydrate or not. The Apply Pressure button applies memory pressure to the application. Clicking the button repeatedly will cause garbage collection eventually and might force the weakly referenced object to be garbage-collected. You can test the application by updating the list box using the Fill List button.

Here is some of the code from 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 tables and 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.

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

WeakReference Class

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

Table 17-1. WeakReference class members

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 returns whether or not the target object has been garbage-collected.

Target

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

Reliable Code

Reliable code helps prevent and handle memory leaks. It is particularly useful in interoperability, where a handle might be shared between managed and native environments. These are the primary reasons to use reliable code:

  • Asynchronous events can cause memory leaks when a catch or a termination handler (a finally block) is interrupted. This is most likely when managed code is deployed in a hosted environment, such as ASP.NET or Microsoft SQL Server. When interrupted, cleanup is not completed and a leak of memory or resources can occur. You can place a catch or finally block in a constrained execution region (CER), which is a region of reliable code, to prevent any such interruption.

  • Destructors are not guaranteed to be called. When this occurs, memory or resources can be leaked. The solution is to derive the class from the CriticalFinalizerObject class. This places the destructor in a CER, where the CLR guarantees that the destructor will be called.

  • There are several problems with handles and interoperability. For example, a managed function calls into native code using interoperability. While the native code is executing, the managed object is cleaned up and the handle is released. This corrupts the handle. At that time, the outcome of the still-running native code is undetermined. Safe handles protect a native handle by incrementing the handle count while the native method is executing. This prevents an early release and possible handle corruption. Safe handles are derived from the CriticalFinalizerObject, which uses a CER.

Constrained Execution Region

The CER is a region of reliable managed code that is guaranteed to run to completion without interruption. Even an asynchronous exception will not prevent the region from executing to completion. Within the CER, developers are constrained to certain actions. These are actions that will not cause an asynchronous exception, such as a memory exception or thread abort. The CLR performs a variety of checks and does some preparation to ensure that code in the CER runs without interruption. This is a short list of some of the actions that should be avoided in a CER:

  • Boxing

  • Unsafe Code

  • Locks

  • Serialization

  • Calling unreliable code

  • Allocating new objects

In a CER, the CLR delays an asynchronous exception event, such as a thread abort, until the region is exited.

You can create CER regions using the RuntimeHelpers.PrepareConstrainedRegions method. Call the PrepareConstrainedRegions static method immediately prior to a try statement. This places the subsequent catch or finally block in a CER. The RuntimeHelpers class is in the System.Runtime.CompilerServices namespace. The code in the try block is not reliable and can be interrupted. However, the related catch or finally block is within a CER and is uninterruptible. From the catch or finally block in a CER, you can call only methods with a strong reliability contract.

Code in a CER can call only reliable methods. Reliable methods have a reliability contract as defined within the ReliabilityContractAttribute, which is an attribute. This class is found in the System.Runtime.ConstrainedExecution namespace. Reliability contracts can be applied to an individual method, methods of a class, or an entire assembly. The ReliabilityContractAttribute 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. Cer.Success is the highest guarantee and promises that the code will successfully complete, assuming valid input. With the reliability contract, developers make reliability assurances. The CLR does not strictly enforce the reliability constraints. Instead, the CLR relies on developer commitment.

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 has written 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.

A CER is used in a variety of places: with annotated try statements, as described in this section, methods executed using the RuntimeHelpers.ExecuteCodeWithGuaranteedCleanup method, and classes derived from the CriticalFinalizerObject class. CriticalFinalizerObject class is introduced in the next section; but the ExecuteCodeWithGuaranteedCleanup method is outside the scope of this book.

Critical Finalizer Class

Asynchronous exceptions 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. Objects derived from the CriticalFinalizerObject class, which is located in the System.Runtime namespace, have critical finalizers. The finalizer runs uninterrupted in a CER. As such, any method called from the finalizer must have a strong reliability contract. The CLR assures that critical finalizables of critical finalizer objects are executed. 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 managed applications, such as SQL Server. A CLR host can asynchronously interrupt a managed application, which can strand important finalizers. Critical finalizer objects solve this potential problem. During normal garbage collection, regular finalizers always run before critical finalizers.

Safe Handles

When passing handles during interoperability, handles can be mishandled, which can cause leaks or other problems. There are two primary causes:

  • Managed code can call native code through platform invocation. If the native function is passed in a handle, that handle must be managed carefully. While in native code, the managed object could be released. When this occurs, the handle might be improperly released. If that happens, the handle is corrupted, which may cause an exception or other error.

  • As mentioned previously, asynchronous code might prevent finalization. This could prevent a handle from being released, which would cause a handle leak.

The solution is to use safe handles, which are classes derived from SafeHandle. The SafeHandle class itself derives and implements the CriticalFinalizerObject type. SafeHandle is an abstract class and must be inherited in another class. Specifically implemented safe handles include SafeHandle, SafeFileHandle, SafeWaitHandle, SafeHandleMinusOneIsInvalid, and SafeHandleZeroOrMinusOneIsInvalid. SafeHandle is in the System.Runtime.InteropServices namespace. The remaining SafeHandle classes mentioned in this paragraph are found in the Microsoft.Win32.SafeHandles namespace. Safe handles support reference counting. Review the documentation of the SafeHandle class for information on the specific implementation of reference counting.

When passed as a parameter in platform invocation, safe handles increment the reference count. After the call is completed, the reference count is decremented automatically, which prevents the handle from being inadvertently released in managed code during a native call.

In the following code, the PipeHandle class is a safe wrapper for a pipe handle. The class exposes the CreatePipe and the CloseHandle application programming interfaces (APIs) through interoperability. Because the CloseHandle method will be called in a CER, a reliability contract is placed on that method. PipeHandle derives from the SafeHandleMinusOneIsInvalid class for the proper behavior, which is derived from SafeHandle. The finalizer of the base class automatically calls the ReleaseHandle method. The overridden ReleaseHandle method should implement the proper behavior to release the pertinent handle.

In the AnonymousPipe constructor, two PipeHandle instances are initialized, which are a read and write handle. Because these are safe handles, the underlying handle is safer from leaks and corruption. In the following sample code, the DllImport attribute imports a native function:

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 DeviceWrapper {
    static private int count = 0;

    public DeviceWrapper () {
        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();
    }

    ~DeviceWrapper() {
        // 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.

To solve this problem, you can add 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 setup and cleanup for the class, respectively. Each instance of the MyDevice unmanaged resource uses 40 KB 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 manages 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. Names allows you to name the collector. The initial threshold sets the minimal level for possible garbage collection. The maximum threshold sets the level where garbage collection is forced. Hopefully, this will garbage-collect managed resources that are holding unmanaged resources. In the cleanup, these objects will release these resources and lower the handle count.

In the managed classes, define a static instance of the HandleCollector. In the constructor, call HandleCollector.Add. In the cleanup code, 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, which applies pressure, 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;
    }
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.211.166