Финализаторы получают доступ к управляемым материалам - PullRequest
4 голосов
/ 26 июля 2010

Мне хорошо известно, что финализаторы обычно используются для управления неуправляемыми ресурсами. При каких обстоятельствах финализатор может иметь дело с управляемыми?

Насколько я понимаю, присутствие в очереди финализатора предотвратит сбор любого объекта или объектов, на которые он сильно ссылается, но не будет (конечно) защищать их от завершения. В обычном ходе событий, после того, как объект завершен, он будет удален из очереди, и любые объекты, на которые он ссылается, больше не будут защищены от сбора при следующем проходе GC. К тому времени, когда вызывается финализатор, финализаторы могут быть вызваны для любой комбинации объектов, на которые ссылается объект; нельзя полагаться на то, что финализаторы вызываются в какой-либо конкретной последовательности, но ссылки на объекты, которые он хранит, должны быть действительными.

Совершенно очевидно, что финализатор никогда не должен ни получать блокировки, ни пытаться создать новый объект. Предположим, однако, что у меня есть объект, который подписывается на некоторые события, и другой объект, который фактически использует события. Если последний объект получает право на сборку мусора, я хочу, чтобы первый объект отписался от событий как можно скорее. Обратите внимание, что прежний объект никогда не будет допущен к финализации до тех пор, пока никакие подписки на него не будут удерживаться никаким живым объектом.

Было бы целесообразно иметь свободный от блокировок стек связанных списков или очередь объектов, для которых необходимо было отписаться, и чтобы финализатор основного объекта поместил ссылку на другой объект в стеке / очереди? Объект элемента связанного списка должен быть размещен при создании основного объекта (поскольку размещение в финализаторе будет запрещено), и, вероятно, потребуется использовать что-то вроде события таймера для опроса очереди (так как событие отменено пришлось бы запускать вне потока финализатора, и, вероятно, было бы глупо иметь поток, единственная цель которого состояла в том, чтобы ждать, пока что-то появится в очереди финализатора), но если финализатор мог бы безопасно ссылаться на свой предварительно выделенный объект связанного списка и основной объект очереди, связанный с его классом, может позволить отписаться от событий в течение 15 секунд или около того после завершения.

Это было бы хорошей идеей? (Примечания: я использую .net 2.0; также попытка добавления в стек или очередь может несколько раз обернуться в Threading.Interlocked.CompareExchange, но я не ожидаю, что он когда-либо застрянет очень долго).

EDIT

Конечно, любой код, который подписывается на события, должен реализовывать iDisposable, но одноразовые вещи не всегда располагаются должным образом. Если бы они были, в финализаторах не было бы необходимости.

Мой интересный сценарий может выглядеть примерно так: класс, реализующий iEnumerator (из T), перехватывает событие changeNotify своего связанного класса, так что перечисление может быть разумно обработано при изменении базового класса (да, я знаю Microsoft считает, что все перечислители должны просто сдаться, но иногда полезнее будет перечислитель, который может продолжать работать). Вполне возможно, что экземпляр класса мог бы перечисляться много тысяч или даже миллионов раз в течение дней или недель, но не обновляться вообще за это время.

В идеале, перечислитель никогда не будет забыт без утилизации, но перечислители иногда используются в тех случаях, когда «foreach» и «использование» не применимы (например, некоторые перечислители поддерживают вложенное перечисление). Тщательно продуманный финализатор может дать возможность справиться с этим сценарием.

Кстати, я бы потребовал, чтобы любое перечисление, которое должно продолжаться через обновления, использовало универсальный IEnumerable (из T); неуниверсальная форма, которая не обрабатывает iDisposable, должна будет вызвать исключение, если коллекция будет изменена.

Ответы [ 4 ]

3 голосов
/ 26 июля 2010

Предположим, однако, что у меня есть объект, который подписывается на некоторые события, и другой объект, который фактически использует события.Если последний объект получает право на сборку мусора, я хочу, чтобы первый объект отписался от событий как можно скорее.Обратите внимание, что первый объект никогда не получит право на финализацию, пока никакие подписки на него не будут удерживаться никаким живым объектом.

Если «последний объект» - это тот, который использует события, и «бывший»«объект - это тот, кто подписывается на события, у« бывшего »объекта должен быть какой-то способ передать информацию о событии« последнему »объекту, то есть у него будет некоторая ссылка на« последний ».Скорее всего, это не позволит «последнему» объекту из когда-либо быть кандидатом в ГК.


При этом я бы рекомендовал избегать такого типа освобождения управляемого ресурса через финализатор, если это абсолютно необходимо.Архитектура, которую вы описываете, кажется очень хрупкой и очень сложной для понимания.Это, вероятно, лучший кандидат на IDisposable, так как финализатор - это «очистка последнего канала».

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

Кроме того, я бы постарался не иметь единую глобальную коллекцию ссылок на объекты - возможно, было бы более целесообразно, чтобы ваши объекты внутренне просто использовали WeakReference .Как только "последний" объект будет собран, WeakReference "бывшего" объекта больше не будет действительным.В следующий раз, когда будет активирована подписка на событие, если внутренняя WeakReference больше не действительна, вы можете просто отписаться.Нет необходимости в глобальных очередях, списках и т. Д. - он должен просто работать ...

0 голосов
/ 27 июля 2010

Давайте попробуем это снова.Можете ли вы добавить обработчики событий для своего издателя следующим образом:

var pub = new Publisher();
var sub = new Subscriber();
var ref = new WeakReference(sub);

EventHandler handler = null; // gotta do this for self-referencing anonymous delegate

handler = (o,e) =>
{
    if(!ref.IsAlive)
    {
        pub.SomeEvent -= handler; // note the self-reference here, see comment above
        return;
    }


    ((Subscriber)ref.Target).DoHandleEvent();
};

pub.SomeEvent += handler;

Таким образом, ваш делегат не будет хранить прямую ссылку на подписчика и автоматически отсоединяет себя, когда подписчик будет собран.Вы можете реализовать это как частный статический член класса Subscriber (для целей инкапсуляции), просто убедитесь, что он статический, чтобы предотвратить непреднамеренное удержание прямой ссылки на объект "this".

0 голосов
/ 26 июля 2010

Я собираюсь назвать объекты «издатель» и «подписчик» и переформулировать мое понимание проблемы:

В C # издатель (эффективно) будет хранить ссылки на подписчиков, не давая подписчикамбыть мусоромЧто я могу сделать, чтобы объекты подписчика могли собирать мусор без явного управления подписками?

Во-первых, я бы рекомендовал сделать все, что мог , чтобы избежать этой ситуации.Теперь я собираюсь двигаться дальше и предположить, что у вас есть, учитывая, что вы все равно задаете вопрос =)

Далее, я бы порекомендовал перехватить добавление и удаление аксессоров события (событий) издателя и использоватьколлекция WeakReferences.Затем вы можете автоматически отсоединять эти подписки каждый раз, когда вызывается событие.Вот чрезвычайно грубый, непроверенный пример:

private List<WeakReference> _eventRefs = new List<WeakReference>();

public event EventHandler SomeEvent
{
    add
    {
        _eventRefs.Add(new WeakReference(value));
    }
    remove
    {
        for (int i = 0; i < _eventRefs; i++)
        {
            var wRef = _eventRefs[i];
            if (!wRef.IsAlive)
            {
                _eventRefs.RemoveAt(i);
                i--;
                continue;
            }

            var handler = wRef.Target as EventHandler;
            if (object.ReferenceEquals(handler, value))
            {
                _eventRefs.RemoveAt(i);
                i--;
                continue;
            }
        }
    }
}
0 голосов
/ 26 июля 2010

Позвольте мне убедиться, что я понимаю - вас беспокоит утечка от подписчиков событий, которые остаются подписанными на собранного издателя событий?

Если это так, то я не думаю, что вам стоит беспокоиться об этом.

Вот что я имею в виду, предполагая, что «прежний» объект является подписчиком события, а «последний» объект является издателем события (вызывает событие):

Единственная причина, по которой подписчик (бывший) является «подписанным», заключается в том, что вы создали объект делегата и передали этот делегат издателю («последнему»).

Если вы посмотрите на участников делегата, у него есть ссылка на объект подписчика и на метод на подписчике, который будет выполнен. Таким образом, существует цепочка ссылок, которая выглядит следующим образом: publisher -> делегат -> подписчик (издатель ссылается на делегата, который ссылается на подписчика). Это односторонняя цепочка - у подписчика нет ссылки на делегата.

Таким образом, единственный корень, который удерживает делегата, находится на издателе («последний»). Когда последний становится подходящим для GC, делегат - также. Если ваши подписчики не предпримут каких-либо специальных действий, когда они откажутся от подписки, они будут фактически отменены, когда соберется делегат - утечки нет).

Редактировать

Судя по комментариям суперкатера, проблема в том, что издатель поддерживает подписчика живым.

Если это проблема, то финализаторы вам не помогут. Причина: у вашего издателя есть реальная, добросовестная ссылка на вашего подписчика (через делегата), и издатель имеет права рутирования (иначе он будет иметь право на GC), поэтому ваши подписчики имеют права рутирования и не будут иметь права на финализацию или GC.

Если у вас возникли проблемы с издателем, поддерживающим жизнь подписчика, я бы посоветовал вам поискать события со слабыми ссылками. Вот пара ссылок, с которых можно начать: http://www.codeproject.com/KB/cs/WeakEvents.aspx http://www.codeproject.com/KB/architecture/observable_property_patte.aspx.

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

Однажды я использовал технику, которая не основывалась на рефлексии. Однако необходимо, чтобы вы могли вносить изменения в код как в издателе, так и в подписчике. Если вы хотите посмотреть образец этого решения, дайте мне знать.

...