Domanda

I'm roughly familiar with the Dispose pattern for non-finalizable types, eg, types that wrap some sort of managed resource that we want to have deterministic cleanup done on. These sort of types typically do not implement a finalizer, since it is completely unnecessary.

However, I'm implementing a C# wrapper for a native API where I'm wrapping multiple, related unmanaged resources, and seemingly, would need multiple classes each implementing the finalizable-dispose pattern. The problem is that the guidelines for the dispose pattern says that finalizable A should not rely on finalizable B, which is exactly what I need:

Dispose Pattern on MSDN:

X DO NOT access any finalizable objects in the finalizer code path, because there is significant risk that they will have already been finalized.

For example, a finalizable object A that has a reference to another finalizable object B cannot reliably use B in A’s finalizer, or vice versa. Finalizers are called in a random order (short of a weak ordering guarantee for critical finalization).

So here are my constraints:

  • To do anything, I have to create an "API" handle.
  • To create "child" handles, I have to provide the API handle in the 'create child' call.
  • Both types of handles can't be leaked.
  • If the API handle is closed, all of its child handles are implicitly closed.
  • To close a child handle, I have to provide the child handle and the API handle in the native call.

The native API looks something like this:

APIHANDLE GizmoCreateHandle();

CHILDHANDLE GizmoCreateChildHandle( APIHANDLE apiHandle );

GizmoCloseHandle( APIHANDLE apiHandle );

GizmoCloseChildHandle( APIHANDLE apiHandle, CHILDHANDLE childHandle);

The naive approach for this would be two parts:

  • For the API handle, follow the typical SafeHandle pattern.
  • For the child handle, follow the typical SafeHandle pattern, but modify it a little bit, since a child handle needs a reference to the API handle in order to implement its ReleaseHandle override - add a method that gives the API handle to the child SafeHandle after it's been constructed.

So everything would look something like this:

    [DllImport( "gizmo.dll" )]
    private static extern ApiSafeHandle GizmoCreateHandle();

    [DllImport( "gizmo.dll" )]
    private static extern void GizmoCloseHandle( IntPtr apiHandle );

    [DllImport( "gizmo.dll" )]
    private static extern ChildSafeHandle GizmoCreateChildHandle(ApiSafeHandle apiHandle);

    [DllImport( "gizmo.dll" )]
    private static extern void GizmoCloseChildHandle( ApiSafeHandle apiHandle, IntPtr childHandle );

    [DllImport( "gizmo.dll" )]
    private static extern void GizmoChildModify( ChildSafeHandle childHandle, int flag );

    public class ApiSafeHandle : SafeHandle
    {
        public ApiSafeHandle() : base( IntPtr.Zero, true ) { }

        public override bool IsInvalid
        {
            get { return this.handle == IntPtr.Zero; }
        }

        protected override bool ReleaseHandle()
        {
            GizmoCloseHandle( this.handle );
            return true;
        }
    }

    public class ChildSafeHandle : SafeHandle
    {
        private ApiSafeHandle apiHandle;

        public ChildSafeHandle() : base( IntPtr.Zero, true ) { }

        public override bool IsInvalid
        {
            get { return this.handle == IntPtr.Zero; }
        }

        public void SetParent( ApiSafeHandle handle )
        {
            this.apiHandle = handle;
        }

        // This method is part of the finalizer for SafeHandle.
        // It access its own handle plus the API handle, which is also a SafeHandle
        // According to MSDN, this violates the rules for finalizers.
        protected override bool ReleaseHandle()
        {
            if ( this.apiHandle == null )
            {
                // We were used incorrectly - we were allocated, but never given 
                // the means to deallocate ourselves
                return false;
            }
            else if ( this.apiHandle.IsClosed )
            {
                // Our parent was already closed, which means we were implicitly closed.
                return true;
            }
            else
            {
                GizmoCloseChildHandle( apiHandle, this.handle );
                return true;
            }
        }
    }

    public class GizmoApi
    {
        ApiSafeHandle apiHandle;

        public GizmoApi()
        {
            this.apiHandle = GizmoCreateHandle();
        }

        public GizmoChild CreateChild()
        {
            ChildSafeHandle childHandle = GizmoCreateChildHandle( this.apiHandle );

            childHandle.SetParent( this.apiHandle );

            return new GizmoChild( childHandle );
        }
    }

    public class GizmoChild
    {
        private ChildSafeHandle childHandle;

        internal GizmoChild( ChildSafeHandle handle )
        {
            this.childHandle = handle;
        }

        public void SetFlags( int flags )
        {
            GizmoChildModify( this.childHandle, flags );
        }

        // etc.
    }

However, now I have a flaw - my ChildSafeHandle's ReleaseHandle is referencing another handle to do its work. I now have two finalizable disposable objects, where one's finalizer depends on the other. MSDN explicitly says that finalizers should not rely on other finalizable objects, since they may already be finalized, or if .Net were to ever support multi-threaded finalization, they might be getting finalized concurrently.

What is the right way to do this? Do the rules allow me to access finalizable object A from the finalizer of B, so long as I test for validity first? MSDN isn't clear on this.

È stato utile?

Soluzione

The important thing to remember about finalization is that the garbage collector guarantees not to trash the fields associated with any object unless it can guarantee that no reference will ever exist to that object, but makes no guarantee as to the sequence or threading context with which it will call Finalize on an object, nor does it have any control over what a Finalize method (or any other) might do with an object.

If foo holds a reference to bar, and no reference to either exists anywhere else except in the Finalization Queue, the system may call Finalize on bar before or after it calls Finalize on foo. If bar and foo hold references to each other, and both agree how to coordinate cleanup it may be possible whichever one has its Finalize method called first to cleanup both objects in whichever sequence the type semantics require. There is no general accepted pattern for doing that, however; anyone implementing such a thing must arrange their own coordination.

Note also that WeakReference has a rather annoying quirk: if one passes true to the constructor of a WeakReference, the fact that its target has a registered finalizer will prevent it from being invalidated until that finalizer has run or become unregistered, but if no strong reference exists to the WeakReference itself, its finalizer will invalidate it even if the target is still valid. Thus, in the scenario above, if bar holds a WeakReference to foo, there would be three finalizers involved: that of foo, that of bar, and that of bar's WeakReference. If foo is supposed to get cleaned up first, the only way bar could accomplish that is by either holding a strong reference, or storing a WeakReference someplace that's accessible via a static reference [which creates dangers of its own].

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top