Использование WeakReference для решения проблемы с незарегистрированными обработчиками событий .NET, вызывающими утечки памяти - PullRequest
11 голосов
/ 12 мая 2010

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

Пример:

    class Foo
    {
        public event Action AnEvent;
        public void DoEvent()
        {
            if (AnEvent != null)
                AnEvent();
        }
    }        
    class Bar
    {
        public Bar(Foo l)
        {
            l.AnEvent += l_AnEvent;
        }

        void l_AnEvent()
        {

        }            
    }

Если я создаю экземпляр Foo и передаю его новому конструктору Bar, а затем отпускаю объект Bar, он не будет освобожден сборщиком мусора из-за регистрации AnEvent.

Я считаю это утечкой памяти, и похоже на мои старые C ++ дни. Конечно, я могу сделать Bar IDisposable, отменить регистрацию события в методе Dispose () и обязательно вызвать Dispose () для его экземпляров, но почему я должен это делать?

У меня первый вопрос, почему события реализуются с сильными ссылками? Почему бы не использовать слабые ссылки? Событие используется для абстрактного уведомления объекта об изменениях в другом объекте. Мне кажется, что если экземпляр обработчика событий больше не используется (т. Е. Нет ссылок на не-события на объект), то любые события, с которыми он зарегистрирован, должны автоматически быть незарегистрированными. Чего мне не хватает?

Я посмотрел на WeakEventManager. Вау, какая боль. Мало того, что его очень сложно использовать, но его документация неадекватна (см. http://msdn.microsoft.com/en-us/library/system.windows.weakeventmanager.aspx - обратите внимание на раздел «Примечания для наследников», в котором есть 6 неопределенно описанных маркеров).

Я видел другие дискуссии в разных местах, но ничего, что я чувствовал, я не мог использовать. Я предлагаю более простое решение на основе WeakReference, как описано здесь. Мой вопрос: не соответствует ли это требованиям значительно меньшей сложности?

Для использования решения приведенный выше код изменяется следующим образом:

    class Foo
    {
        public WeakReferenceEvent AnEvent = new WeakReferenceEvent();

        internal void DoEvent()
        {
            AnEvent.Invoke();
        }
    }

    class Bar
    {
        public Bar(Foo l)
        {
            l.AnEvent += l_AnEvent;
        }

        void l_AnEvent()
        {

        }
    }

Обратите внимание на две вещи: 1. Класс Foo изменяется двумя способами: событие заменяется экземпляром WeakReferenceEvent, показанным ниже; и вызов события изменяется. 2. Класс бара НЕИЗМЕНЕН.

Нет необходимости создавать подклассы WeakEventManager, реализовывать IWeakEventListener и т. Д.

ОК, так что до реализации WeakReferenceEvent. Это показано здесь. Обратите внимание, что он использует общий WeakReference , который я позаимствовал здесь: http://damieng.com/blog/2006/08/01/implementingweakreferencet

class WeakReferenceEvent
{
    public static WeakReferenceEvent operator +(WeakReferenceEvent wre, Action handler)
    {
        wre._delegates.Add(new WeakReference<Action>(handler));
        return wre;
    }

    List<WeakReference<Action>> _delegates = new List<WeakReference<Action>>();

    internal void Invoke()
    {
        List<WeakReference<Action>> toRemove = null;
        foreach (var del in _delegates)
        {
            if (del.IsAlive)
                del.Target();
            else
            {
                if (toRemove == null)
                    toRemove = new List<WeakReference<Action>>();
                toRemove.Add(del);
            }
        }
        if (toRemove != null)
            foreach (var del in toRemove)
                _delegates.Remove(del);
    }
}

Функциональность тривиальна. Я переопределяю оператор +, чтобы получить + = синтаксические события соответствия сахара. Это создает WeakReferences для делегата Action. Это позволяет сборщику мусора освободить целевой объект события (в данном примере Bar), когда никто больше не удерживает его.

В методе Invoke () просто пропустите слабые ссылки и вызовите их Target Action. Если найдены какие-либо мертвые (то есть, собранные мусор) ссылки, удалите их из списка.

Конечно, это работает только с делегатами типа Action. Я попытался сделать этот универсальный, но столкнулся с отсутствующим, где T: делегат в C #!

В качестве альтернативы просто измените класс WeakReferenceEvent на WeakReferenceEvent и замените Action на Action . Исправьте ошибки компилятора, и у вас есть класс, который можно использовать так:

    class Foo
    {
        public WeakReferenceEvent<int> AnEvent = new WeakReferenceEvent<int>();

        internal void DoEvent()
        {
            AnEvent.Invoke(5);
        }
    }

Полный код с и оператором (для удаления событий) показан здесь:

class WeakReferenceEvent<T>
{
    public static WeakReferenceEvent<T> operator +(WeakReferenceEvent<T> wre, Action<T> handler)
    {
        wre.Add(handler);
        return wre;
    }
    private void Add(Action<T> handler)
    {
        foreach (var del in _delegates)
            if (del.Target == handler)
                return;
        _delegates.Add(new WeakReference<Action<T>>(handler));
    }

    public static WeakReferenceEvent<T> operator -(WeakReferenceEvent<T> wre, Action<T> handler)
    {
        wre.Remove(handler);
        return wre;
    }
    private void Remove(Action<T> handler)
    {
        foreach (var del in _delegates)
            if (del.Target == handler)
            {
                _delegates.Remove(del);
                return;
            }
    }

    List<WeakReference<Action<T>>> _delegates = new List<WeakReference<Action<T>>>();

    internal void Invoke(T arg)
    {
        List<WeakReference<Action<T>>> toRemove = null;
        foreach (var del in _delegates)
        {
            if (del.IsAlive)
                del.Target(arg);
            else
            {
                if (toRemove == null)
                    toRemove = new List<WeakReference<Action<T>>>();
                toRemove.Add(del);
            }
        }
        if (toRemove != null)
            foreach (var del in toRemove)
                _delegates.Remove(del);
    }
}

Надеюсь, это поможет кому-то другому, когда они столкнутся с загадочным событием, вызвавшим утечку памяти в мире, где собирается мусор!

Ответы [ 4 ]

2 голосов
/ 17 мая 2010

Конечно, это повлияет на производительность.

Это немного похоже на то, зачем ссылаться на другие сборки в моем решении, когда я могу использовать отражение для динамического чтения сборки и выполнения соответствующих вызовов в ее типах?

Итак, вкратце ... Вы используете сильную ссылку по 2 причинам ... 1. Тип безопасности (не совсем применимо здесь) 2. Производительность

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

Еще одна мысль ... Что, если вы прикрепили обработчик события, чтобы сказать событие com-объекта? Технически этот объект не управляется, так как же он узнает, когда он нуждается в очистке, конечно, это сводится к тому, как фреймворк правильно обрабатывает область?

Этот пост поставляется с "он работает в моей голове гарантии", не несет никакой ответственности за то, как этот пост изображается:)

2 голосов
/ 12 мая 2010

Я нашел ответ на свой вопрос о том, почему это не работает. Да, действительно, я упускаю незначительную деталь: вызов + = для регистрации события (l.AnEvent + = l_AnEvent;) создает неявный объект Action. Этот объект обычно поддерживается только самим событием (и стеком вызывающей функции). Таким образом, когда вызов возвращается и запускается сборщик мусора, неявно созданный объект Action освобождается (теперь на него указывает только слабая ссылка), а событие не регистрируется.

(болезненное) решение - хранить ссылку на объект Action следующим образом:

    class Bar
    {
        public Bar(Foo l)
        {
            _holdAnEvent = l_AnEvent;
            l.AnEvent += _holdAnEvent;
        }
        Action<int> _holdAnEvent;
        ...
    }

Это работает, но устраняет простоту решения.

0 голосов
/ 01 июля 2015

Если у вас есть обработчик событий, у вас есть два объекта:

  1. Объект вашего класса. (Экземпляр foo)
  2. Объект, представляющий ваш обработчик событий. (Например, экземпляр EventHandler или экземпляр Action.)

Причина, по которой вы думаете, что у вас утечка памяти, заключается в том, что объект EventHandler (или Action) внутренне содержит ссылку на ваш объект Foo. Это предотвратит сбор вашего объекта Foo.

Теперь, почему вы не можете написать WeakEventHandler? Ответ вы можете, но вы должны убедиться, что:

  1. Ваш делегат (экземпляр EventHandler или Action) никогда не имеет жесткой ссылки на ваш объект Foo
  2. Ваш WeakEventHandler содержит сильную ссылку на ваш делегат и слабую ссылку на ваш объект Foo
  3. У вас есть условия для отмены регистрации вашего WeakEventHandler, когда слабая ссылка становится недействительной. Это потому, что нет способа узнать, когда объект собран.

На практике это того не стоит. Это потому, что у вас есть компромиссы:

  • Ваш метод обработчика событий должен быть статическим и принимать объект в качестве аргумента, таким образом, он не содержит строгой ссылки на объект, который вы хотите собрать.
  • Ваши объекты WeakEventHandler и Action подвергаются высокому риску при переходе в Gen 1 или 2. Это вызовет высокую нагрузку на сборщик мусора.
  • Слабые ссылки содержат дескриптор GC. Это может негативно повлиять на производительность.

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

0 голосов
/ 18 июля 2011

Существует два шаблона, которые я знаю для создания слабых подписок на события: один - чтобы подписчик события имел сильную ссылку на делегата, который указывает на него, в то время как издатель держит слабую ссылку на этого делегата. Это имеет недостаток, заключающийся в том, что все события должны выполняться по слабой ссылке; оно может не давать издателю уведомления о том, истек ли срок каких-либо событий.

Другой подход - дать каждому, кто на самом деле заинтересован в объекте, ссылку на обертку, которая, в свою очередь, содержит ссылку на «кишки»; обработчик событий имеет ссылку только на «кишки», а в кишках нет строгой ссылки на оболочку. Оболочка также содержит ссылку на объект, метод Finalize которого будет отписываться от события (самый простой способ сделать это - иметь простой класс, метод Finalize которого вызывает Action со значением False, а метод Dispose которого вызывает делегировать со значением True и подавляет завершение).

Этот подход имеет недостаток, заключающийся в том, что все операции, не связанные с событиями, в главном классе должны выполняться через оболочку (очень строгая ссылка), но избегает необходимости использовать какие-либо WeakReferences для чего-либо, кроме подписки на события и отписки. К сожалению, использование этого подхода со стандартными событиями требует, чтобы (1) любой класс, публикующий события, на которые подписывается, должен иметь поточно-безопасный (предпочтительно также без блокировки) обработчик «remove», который можно безопасно вызывать из потока Finalize и (2) все объекты, на которые прямо или косвенно ссылается объект, используемый для отмены подписки на событие, будут оставаться полуживыми до тех пор, пока GC не пройдет после запуска финализатора. Использование другой парадигмы события (например, подписка на событие путем вызова функции, возвращающей IDisposable, который можно использовать для отмены подписки) может обойти эти ограничения.

...