Автор: Sergey Teplyakov

Камрад @v2_matveev указал на прекрасную реализацию слабых событий в коде Roslyn-а.

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

Решается эта проблема разными способами. Самый правильный способ не использовать события в синглтонах. Второй способ отписываться от событий вовремя. Третий способ хипстерский, воспользоваться той или иной реализацией Weak Event Pattern-а, например, с помощью WeakEventManager или чего-то подобного. Но раз уж зашла речь за слыбые события, то их можно реализовать по разному и далее показана самая изящная реализация.

Вначале демонстрируем проблему: добавляем синглтон с событиями и короткоживущий объект, который на них подписывается:

class LongLivedEventProvider
{
public static readonly LongLivedEventProvider Instance =
new LongLivedEventProvider(),
public event EventHandler<,EventArgs>, Event,
public void RaiseEvent()
{
Event
?.Invoke(this, EventArgs.Empty),
}
}
class ShortLivedEventHandler
{
public void Subscribe()
{
EventHandler<,EventArgs>, handler = (sender, args) =>, EventHandler(),
// Отсутствие отписки от события приведет к утеччке памяти
LongLivedEventProvider.Instance.Event +=
handler,
}
private void EventHandler()
{
Console.WriteLine('Обрабатываем событие!'),
}
}

Следующий пример показывает проблему в действии:var shortLived = new ShortLivedEventHandler(),
// Подписываемся на событие обычным образом!
shortLived.Subscribe(),
// Создаем слабую ссылку, чтобы отслеживать время жизни короткоживущего объекта
var firstWeakReference = new WeakReference(shortLived, false
),
Console.WriteLine('Зажигаем событие'),
LongLivedEventProvider.Instance.
RaiseEvent(),
Console.WriteLine('А жив ли обджект? ' + firstWeakReference.
IsAlive),
Console.WriteLine('Собираем мусор'),
GC.
Collect(),
Console.WriteLine('А жив ли обджек? ' + firstWeakReference.IsAlive),

Запускаем код в релизном режиме и без отладчика (в противном случае время жизни объекта shortLived будет продлено и пример не покажет результата). И получаем:

Зажигаем событие
Обрабатываем событие
А жив ли обджект? True
Собираем мусор
А жив ли обджект? True

Наш объект выжил, что является ожидаемым поведением

ПРИМЕЧАНИЕ
Если вы, вдруг, подумали, что тут нужно поставить скобочки или каким-то еще способом указать среде исполнения, что ссылка shortLived должна быть недостижима, то, поздравляю, вы открыли для себя что-то новое. Делать этого не нужно. Подробности в заметке О сборке мусора и достижимости объектов.

Для реализация своих слабых подписчиков, нам нужен следующий класс:internal static class WeakEventHandler<,TArgs>,
{
public static EventHandler<,TArgs>, Create<,THandler>,(
THandler handler, Action<,THandler, object, TArgs>, invoker)
where THandler : class
{
var weakEventHandler = new WeakReference<,THandler>,(handler),
return (sender, args) =>,
{
THandler thandler,
if (weakEventHandler.TryGetTarget(out thandler))
{
invoker(thandler, sender, args),
}
},
}
}

И метод SubscribeWeakly в классе ShortLivedEventHandler:public void SubscribeWeakly()
{
// Не используем 'this' в лямбде.
// Неявно захваченный 'this' будет строгой ссылкой
var handler = WeakEventHandler<,EventArgs>,.
Create(
this, (@this, o, args) =>, @this.EventHandler()),
// Теперь можно и не отписываться от события.
// Во всяком случае текущий объект будет собыран сборщиком
LongLivedEventProvider.Instance.Event += handler,
}

Теперь, если мы будем подписываться с помощью SubscribeWeakly, то синглтон уже не будет держать корневую ссылку на короткоживующий объект, что позволит последнему покинуть этот мир своевременно:var shortLived = new ShortLivedEventHandler(),
var firstWeakReference = new WeakReference(shortLived, false
),
shortLived
.SubscribeWeakly(),
LongLivedEventProvider.Instance.
RaiseEvent(),
GC.Collect(),
// Теперь свойство IsAlive вернет false!
Console.WriteLine('А жив ли обджект? ' + firstWeakReference.IsAlive),

Идея этого решения в следующем: метод WeakEventHandler.Create заменяет экземплярный обработчик события статическим с явным протаскиванием параметра экземпляра. Обратите внимание, что лямбда в методе SubscribeWeakly не захватывает this, а вызывает экземплярный метод HandleEvent лишь через параметр @this, который был получен у слабой ссылки.

Несмотря на то, что этот подход устраняет проблему продления времени жизни объектов ShortLivedEventHandler, он не решает проблему увеличения числа подписчиков в глобальном объекте. LongLivedEventProvider все еще будет хранить все обработчики событий, просто делать они ничего не будут. Что также является утечкой памяти. И при зажигании события, долгоживущему объекту все равно придется перебирать и вызвать все обработчики, что также скажется на производительности, если приложение работает 24/7.

Это я все к тому, что этот подход все равно не применим для синглтонов, которые по-прежнему НЕ ДОЛЖНЫ содержать событий, но может применяться в подходах со среднеживущими объектами.

Дополнительные ссылки
  • WeakEventhandler at Roslyn codebase
  • Замыкания в языке C#
  • О сборке мусора и достижимости объектов


Помогла статья? Оцените её!
0 из 5. Общее количество голосов - 0
 

You have no rights to post comments

Дмитрий Крикунов

Публикую статьи, обучающие курсы и новости по программированию: алгоритмам, языкам (С++, Java), параллельному программированию, паттернам и библиотекам (Qt, boost).