Реализовать финализируемый шаблон удаления с несколькими связанными финализируемыми объектами.

Я примерно знаком с шаблоном Dispose для незавершаемых типов, например, типов, которые обертывают какой-то управляемый ресурс, для которого мы хотим выполнить детерминированную очистку. Типы такого рода обычно не реализуют финализатор, так как он совершенно не нужен.

Однако я реализую оболочку C# для собственного API, где я оборачиваю несколько связанных неуправляемых ресурсов и, по-видимому, потребуется несколько классов, каждый из которых реализует шаблон finalizable-dispose. Проблема в том, что в рекомендациях по шаблону удаления говорится, что финализируемый A не должен полагаться на финализируемый B, а это именно то, что мне нужно:

Шаблон удаления в MSDN:

X ЗАПРЕЩАЕТСЯ обращаться к финализируемым объектам в пути кода финализатора, так как существует значительный риск того, что они уже финализированы.

Например, финализируемый объект A, который имеет ссылку на другой финализируемый объект B, не может надежно использовать B в финализаторе A, или наоборот. Финализаторы вызываются в случайном порядке (за исключением слабой гарантии упорядочения для критической финализации).

Итак, вот мои ограничения:

  • Чтобы что-то сделать, мне нужно создать дескриптор «API».
  • Чтобы создать «дочерние» дескрипторы, я должен предоставить дескриптор API в вызове «создать дочерний».
  • Оба типа ручек не могут протекать.
  • Если дескриптор API закрыт, все его дочерние дескрипторы неявно закрыты.
  • Чтобы закрыть дочерний дескриптор, я должен предоставить дочерний дескриптор и дескриптор API в собственном вызове.

Нативный API выглядит примерно так:

APIHANDLE GizmoCreateHandle();

CHILDHANDLE GizmoCreateChildHandle( APIHANDLE apiHandle );

GizmoCloseHandle( APIHANDLE apiHandle );

GizmoCloseChildHandle( APIHANDLE apiHandle, CHILDHANDLE childHandle);

Наивный подход для этого будет состоять из двух частей:

  • Для дескриптора API следуйте типичному шаблону SafeHandle.
  • Для дочернего дескриптора следуйте типичному шаблону SafeHandle, но немного измените его, поскольку дочернему дескриптору требуется ссылка на дескриптор API, чтобы реализовать его переопределение ReleaseHandle — добавьте метод, который дает дескриптор API дочернему SafeHandle после он был построен.

Таким образом, все будет выглядеть примерно так:

    [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.
    }

Однако теперь у меня есть недостаток — ReleaseHandle моего ChildSafeHandle ссылается на другой дескриптор для выполнения своей работы. Теперь у меня есть два финализируемых одноразовых объекта, где финализатор одного зависит от другого. MSDN прямо говорит, что финализаторы не должны полагаться на другие финализируемые объекты, поскольку они могут быть уже финализированы, или, если .Net когда-либо будет поддерживать многопоточную финализацию, они могут финализироваться одновременно.

Как правильно это сделать? Позволяют ли мне правила получить доступ к завершаемому объекту A из финализатора B, если я сначала проверяю его на достоверность? MSDN не ясно об этом.


person antiduh    schedule 23.12.2013    source источник
comment
Есть веская причина, по которой нам нравится, когда пользователи публикуют фрагменты. Как есть, это расплывчато, но не похоже, что происходит что-то особенное. Во время финализации просто не имеет значения, что финализируемый объект имеет ссылку на другой финализируемый объект. Вы заботитесь об этом, когда реализуете IDisposable, вот и все.   -  person Hans Passant    schedule 24.12.2013
comment
Ключевой вопрос: предположим, что родительский дескриптор завершается до дочернего дескриптора. Приведет ли это к сбою финализации дочернего элемента?   -  person Eric Lippert    schedule 24.12.2013
comment
Если собственный родительский дескриптор закрывается перед собственным дочерним дескриптором, то все очищается, и любая финализация/удаление дочернего элемента в .Net-land не должна ничего делать.   -  person antiduh    schedule 24.12.2013
comment
И комментатор, и близкая причина говорят вам, почему: проблемы с кодом должны включать кратчайший объем кода, необходимый для воспроизведения проблемы - в самом вопросе. Ссылка на сторонний ресурс, на котором размещен проект, не допускается. Вот почему.   -  person George Stocker    schedule 29.06.2014


Ответы (1)


Важно помнить о финализации: сборщик мусора гарантирует, что поля, связанные с каким-либо объектом, не будут уничтожены, если только он не может гарантировать, что на этот объект никогда не будет существовать ссылка, но не гарантирует, что последовательность или контекст потоков, с которым он будет вызывать Finalize для объекта, и при этом он не имеет никакого контроля над тем, что метод Finalize (или любой другой) может делать с объектом.

Если foo содержит ссылку на bar, и ни одна из них не существует где-либо еще, кроме очереди завершения, система может вызвать Finalize для bar до или после вызова Finalize для foo. Если bar и foo содержат ссылки друг на друга, и оба договариваются о том, как координировать очистку, может оказаться возможным, в зависимости от того, какой из них имеет свой метод Finalize, вызванный первым, для очистки обоих объектов в любой последовательности, требуемой семантикой типа. Однако для этого не существует общепринятой схемы; любой, кто реализует такую ​​вещь, должен организовать собственную координацию.

Также обратите внимание, что у WeakReference есть довольно раздражающая особенность: если передать true конструктору WeakReference, тот факт, что его цель имеет зарегистрированный финализатор, не позволит сделать его недействительным до тех пор, пока этот финализатор не запустится или не станет незарегистрированным, но< /em> если на сам WeakReference не существует строгой ссылки, его финализатор сделает его недействительным, даже если цель все еще действительна. Таким образом, в приведенном выше сценарии, если bar содержит от WeakReference до foo, будут задействованы три финализатора: foo, bar и bar WeakReference. Если предполагается, что foo будет очищен первым, единственный способ, которым bar может это сделать, — либо сохранить строгую ссылку, либо сохранить WeakReference в каком-то месте, доступном через статическую ссылку [что само по себе создает опасности].

person supercat    schedule 24.12.2013
comment
Вы - маяк здравомыслия. Спасибо за ваш ответ. - person antiduh; 24.12.2013