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

Проблема: зарегистрированные обработчики событий создают ссылку из события на экземпляр обработчика события. Если этот экземпляр не может отменить регистрацию обработчика событий (возможно, через 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.

Я считаю, что это утечка памяти, и похоже на мои старые С++-дни. Я могу, конечно, сделать 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. Класс Bar НЕОБХОДИМО.

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

ОК, так что для реализации WeakReferenceEvent. Это показано здесь. Следует отметить, что он использует общий WeakReference <T> что я заимствовал отсюда: 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() просто пропустите слабые ссылки и вызовите их целевое действие. Если найдены мертвые (т.е. Собранные мусором) ссылки, удалите их из списка.

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

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

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

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

Полный код с <T> , и оператор - (для удаления событий) показан здесь:

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);
    }
}

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

Ответ 1

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

A (болезненное) решение состоит в том, чтобы удерживать ссылку на объект Action следующим образом:

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

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

Ответ 2

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

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

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

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

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

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

Ответ 3

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

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

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

Ответ 4

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

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

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

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

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

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

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

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