С# все еще привязан к событию после отключения

В настоящее время я отлаживаю большое (очень большое!) С#-приложение, которое содержит утечки памяти. Он в основном использует Winforms для графического интерфейса, хотя несколько элементов управления созданы в WPF и размещены на ElementHost. До сих пор я обнаружил, что многие утечки памяти были вызваны тем, что события не были отцеплены (путем вызова -=), и мне удалось решить эту проблему.

Однако я только что столкнулся с подобной проблемой. Существует класс WorkItem (короткоживущий), который в конструкторе регистрируется на события другого класса с именем ClientEntityCache (долгоживущий). События никогда не отключались, и я мог видеть в профилировщике .NET, что экземпляры WorkItem сохранялись, когда они не должны были из-за этих событий. Поэтому я решил реализовать IDisposable в WorkItem, а в функции Dispose() я отвязываю события следующим образом:

public void Dispose()
{
  ClientEntityCache.EntityCacheCleared -= ClientEntityCache_CacheCleared;
  // Same thing for 10 other events
}

РЕДАКТИРОВАТЬ

Вот код, который я использую для подписки:

public WorkItem()
{
  ClientEntityCache.EntityCacheCleared += ClientEntityCache_CacheCleared;
  // Same thing for 10 other events
}

Я также изменил код для отмены регистрации, чтобы не вызывать новый EntityCacheClearedEventHandler.

КОНЕЦ РЕДАКТИРОВАНИЯ

Я сделал вызовы Dispose в нужных местах кода, который использует WorkItem, и когда я отлаживаю, я вижу, что функция действительно вызывается, и я делаю -= для каждого события. Но я все еще получаю утечку памяти, и мои рабочие элементы все еще остаются в живых после удаления, и в профилировщике .NET я вижу, что экземпляры остаются в живых, потому что обработчики событий (такие как EntityCacheClearedEventHandler) все еще имеют их в своем списке вызовов. Я пытался отцепить их более одного раза (несколько -=), просто чтобы убедиться, что они не были подключены более одного раза, но это не помогает.

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

Спасибо!

РЕДАКТИРОВАТЬ:

Если это поможет, вот корневой путь, описанный профилировщиком .NET: многие вещи указывают на ClientEntityCache, который указывает на EntityCacheClearedEventHandler, который указывает на Object[], который указывает на другой экземпляр EntityCacheClearedEventHandler (я не понимаю, почему), который указывает на WorkItem.


person Carl    schedule 26.05.2011    source источник
comment
Можете ли вы показать нам код подписки на мероприятие?   -  person ChaosPandion    schedule 26.05.2011
comment
Вызывается ли Dispose из разных потоков? Используете ли вы пользовательское добавление/удаление для события? Состояние делегата не может быть повреждено (поскольку оно неизменно), но несколько операций добавления или удаления могут мешать друг другу, если вы реализуете пользовательское добавление/удаление и не включаете блокировку реализации по умолчанию.   -  person Dan Bryant    schedule 26.05.2011
comment
Возможно, вы захотите остановиться в отладчике и взглянуть на список вызовов (Delegate.GetInvocationList) и посмотреть, сколько в нем обработчиков событий. Могут быть подключены обработчики событий, которые связаны не с ClientEntityCache_CacheCleared, а с какой-либо другой функцией.   -  person Chris Taylor    schedule 26.05.2011
comment
Вы уверены, что ничто другое не ссылается на WorkItems? Я не уверен, что реализация IDisposable является лучшим подходом, ресурсы, которые вы пытаетесь удалить, не являются неуправляемыми и должны быть освобождены GC в следующем цикле. Вы пытались просто установить для делегата значение null вместо того, чтобы пытаться удалить обработчики событий? Есть ли несколько обработчиков, связанных с событием?   -  person Marcus King    schedule 26.05.2011
comment
@Маркус Кинг: Да, я уверен, что ничто другое не ссылается на WorkItems. Я вижу в профилировщике .NET, что единственное, что на них ссылается, - это обработчики событий. Для IDisposable я согласен, что это может быть не лучшее решение, но у меня не было способа вызвать, чтобы сообщить WorkItem об отмене регистрации, поэтому я попробовал это. Может быть, мне следует просто создать метод под названием CleanUp или что-то в этом роде.   -  person Carl    schedule 26.05.2011
comment
Если вы вызываете dispose для объекта, то кажется, что вы намерены уничтожить все это, не могли бы вы просто установить для объекта значение null и дождаться сборки мусора или вызвать GC.Collect() и GC.WaitForPendingFinalizers()   -  person Marcus King    schedule 26.05.2011
comment
@Маркус Кинг - НИКОГДА не звоните в GC.Collect. Это огромный запах кода.   -  person vcsjones    schedule 26.05.2011
comment
@vcsjones Я тоже не рекомендую вызывать его, но похоже, что если он действительно беспокоится об использовании памяти и хочет точно контролировать, когда освобождать объекты, другого варианта действительно нет. Это потенциально опасно, но может сработать для него. Вы не можете сказать НИКОГДА не используйте его. MS поместила его в структуру, чтобы его можно было использовать в нужной ситуации. Так это или нет, обсуждается   -  person Marcus King    schedule 26.05.2011
comment
@Marcus King: установка объекта в значение null уже была выполнена в коде, и я все еще делаю это после вызова моей функции Dispose. Но тем не менее, если я запускаю свое приложение в течение часа или около того, я получаю 100 рабочих элементов, когда мне действительно нужно только 1 или 2 за раз. Итак, я проверил, в чем проблема, и профилировщик .NET сказал мне, что все они остаются в живых, даже если установлено значение null из-за событий из ClientEntityCache, от которых я, похоже, не могу отменить регистрацию. Я не вызываю GC.collect() и не хочу этого делать, потому что мне все равно, когда будут собраны WorkItems, я просто хочу, чтобы они когда-нибудь были собраны.   -  person Carl    schedule 26.05.2011


Ответы (5)


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

// Simple class to host the Event
class Test
{
  public event EventHandler MyEvent;
}

// Two different methods which will be wired to the Event
static void MyEventHandler1(object sender, EventArgs e)
{
  throw new NotImplementedException();
}

static void MyEventHandler2(object sender, EventArgs e)
{
  throw new NotImplementedException();
}


[STAThread]
static void Main(string[] args)
{
  Test t = new Test();
  t.MyEvent += new EventHandler(MyEventHandler1);
  t.MyEvent += new EventHandler(MyEventHandler2); 

  // Break here before removing the event handler and inspect t.MyEvent

  t.MyEvent -= new EventHandler(MyEventHandler1);      
  t.MyEvent -= new EventHandler(MyEventHandler1);  // Note this is again MyEventHandler1    
}

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

введите здесь описание изображения

Теперь, после удаления MyEventHandler1 дважды, MyEventHandler2 по-прежнему зарегистрирован, поскольку остался только один делегат, он выглядит немного иначе, он больше не отображается в списке, но пока делегат для MyEventHandler2 не будет удален, на него все равно будет ссылаться событие .

введите здесь описание изображения

person Chris Taylor    schedule 26.05.2011
comment
Вы хотите сказать, что ему нужно отключить все обработчики, чтобы высвободить ресурсы? - person Marcus King; 26.05.2011
comment
Спасибо за хорошие примеры, но, к сожалению, это не так. После отмены регистрации на событие список вызовов становится нулевым. Итак, я думаю, что все кажется в порядке, но я все еще не понимаю, почему профилировщик .NET сообщает мне, что WorkItem поддерживается живым из-за ссылки из EntityCacheClearedEventHandler. - person Carl; 26.05.2011
comment
Что ж, если целевой объект сохраняется, потому что событие все еще ссылается на обработчик в целевом объекте, тогда да, ему нужно будет удалить все обработчики, чтобы событие больше не содержало ссылку на целевой объект, и целевой объект мог быть выпущеным. Конечно, мы не видим весь код, поэтому я пытаюсь предоставить @Carl инструменты для исследования этого аспекта проблемы. - person Chris Taylor; 26.05.2011
comment
@Карл, будь осторожен. Если есть только один делегат, то _invocationList имеет значение null, что может ввести в заблуждение. Смотрите второй снимок экрана, я не показываю _invocationList, потому что он нулевой, но событие не равно нулю, оно по-прежнему ссылается на MyEventHandler2 и будет делать это до тех пор, пока вы не скажете t.MyEvent -= new EventHandler(MyEventHandler2). Конечно, это предполагает, что это действительно проблема, с которой вы столкнулись. - person Chris Taylor; 26.05.2011
comment
О да, ты прав. Я только что увидел, что событие по-прежнему ссылается на метод другого класса ActivityWorkItem (который является производным от WorkItem). Я проверил, и есть экземпляры ActivityWorkItem и экземпляры WorkItem, которые регистрируются в событиях ClientEntityCache. Я правильно отменяю их регистрацию в WorkItem, но еще не сделал этого для ActivityWorkItem. Возможно ли, что мои экземпляры WorkItem остаются в живых, потому что экземпляры ActivityWorkItem не отменяют регистрацию в событии? Должен сказать, мне трудно понять дизайн парня, который создал это приложение! - person Carl; 26.05.2011
comment
@Карл, очень сложно не видеть больше кода. Но в основном, и я уверен, что вы это знаете, событие будет содержать ссылку на все экземпляры объекта, у которых есть метод обработчика, прикрепленный к событию. Таким образом, эти объекты не будут подлежать сбору, пока они не удалят себя из событий, на которые они подписаны. Конечно, если у каждого из трех объектов есть подписка, и только один из объектов нужно восстановить, только этот объект должен отказаться от подписки, чтобы ссылка больше не удерживалась на этот объект. Таким образом, вам нужно будет определить все ссылочные зависимости... - person Chris Taylor; 26.05.2011
comment
...чтобы точно знать, что поддерживает жизнь объекта. Если бы я столкнулся с этой проблемой, я бы использовал WinDBG, чтобы сделать быстрый дамп памяти, а затем использовать команду !gcroot, чтобы найти все ссылки, которые все еще поддерживаются на целевой объект. Но это довольно продвинуто, и это было бы последним средством. - person Chris Taylor; 26.05.2011
comment
Ладно спасибо большое за помощь! Я попытаюсь взглянуть на WinDBG, но я никогда не использовал его. - person Carl; 26.05.2011
comment
Есть еще вещь, которую я не понимаю. Я несколько раз сталкивался с этой проблемой обработчика событий, и результатом профилировщика памяти .NET было то, что MyEventHandler имел поле _target, установленное на MyInstance, и поэтому MyInstance оставался в живых. Теперь я получил MyEventHandler, _invocationList которого указывает на массив объектов, а один из этих объектов — это другой MyEventHandler, поле _target которого указывает на MyInstance. (Я не уверен, что должен оставить это как комментарий, но я все еще новичок в StackOverflow и не знаю, куда еще его поместить, лол). - person Carl; 26.05.2011
comment
Кажется, когда я отлаживаю, я вижу, что обработчик событий НЕ сохраняет ссылку на мои рабочие элементы. Я не понимаю, почему профилировщик .NET говорит мне, что он есть, но похоже, что моя проблема должна быть решена, если я доверяю отладчику. - person Carl; 26.05.2011

При отсоединении события он должен быть тем же делегатом. Как это:

public class Foo
{
     private MyDelegate Foo = ClientEntityCache_CacheCleared;
     public void WorkItem()
     {
         ClientEntityCache.EntityCacheCleared += Foo;
     }

     public void Dispose()
     {
         ClientEntityCache.EntityCacheCleared -= Foo;
     }
}

Причина в том, что вы используете для этого синтаксический сахар:

public class Foo
{
     public void WorkItem()
     {
         ClientEntityCache.EntityCacheCleared +=
new MyDelegate(ClientEntityCache_CacheCleared);
     }

     public void Dispose()
     {
         ClientEntityCache.EntityCacheCleared -=
new MyDelegate(ClientEntityCache_CacheCleared);
     }
}

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

person vcsjones    schedule 26.05.2011
comment
@vcjones, это не так, это не обязательно должен быть один и тот же экземпляр делегата. Просто это должен быть тот же метод, что и исходный делегат. - person Chris Taylor; 26.05.2011

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

person John Leidegren    schedule 26.05.2011
comment
Я только что попытался сделать += там, где я вызывал -=, и я не получаю ошибок, даже если отладчик проходит через этот код. Значит ли это, что я пытаюсь отменить регистрацию не на том мероприятии или...? - person Carl; 26.05.2011
comment
Да, вы теряете свои ссылки, и поэтому это не работает так, как вы ожидаете. Каждый делегат уникален, и вам необходимо зарегистрироваться и отменить регистрацию, используя одного и того же делегата. - person John Leidegren; 28.05.2011

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

Если вы сами вызвали свой метод Dispose, ссылки выйдут за рамки.

person Noel Kennedy    schedule 26.05.2011
comment
Я сам вызываю свой метод Dispose, когда заканчиваю работу с WorkItems. Я вижу, что он вызывается при отладке. Тем не менее ссылки сохраняются. - person Carl; 26.05.2011
comment
вызов Dispose не приведет к тому, что ссылка выйдет за пределы области видимости. Dispose может очистить внутреннее состояние экземпляра, но это не повлияет на область действия экземпляра. - person Chris Taylor; 26.05.2011
comment
Я предполагаю, что я пытаюсь сказать, что у объекта не будет вызываться метод Dispose (фреймворком), если он поддерживается прослушивателями событий. Следовательно, отключение обработчиков событий в этом методе и ожидание того, что сборщик мусора вызовет его, не сработает. - person Noel Kennedy; 27.05.2011
comment
Если вы вызываете его сами, удаляете свою ссылку, а объекты все еще живы, должен быть другой объект, содержащий ссылку на него. - person Noel Kennedy; 27.05.2011

Возможно, попробуйте:

 public void Dispose()
    {
      ClientEntityCache.EntityCacheCleared -= ClientEntityCache_CacheCleared;
      // Same thing for 10 other events
    }

Вы создаете новый обработчик событий и удаляете его из delegate, который фактически ничего не делает.

Удалите подписку на событие, удалив ссылку на исходный метод события подписки.

Вы всегда можете просто установить eventhandler = delegate {}; На мой взгляд, это будет лучше, чем null.

person IAbstract    schedule 26.05.2011
comment
На самом деле компилятор неявно добавит new EntityCacheClearedEventHandler(ClientEntityCache_CacheCleared). - person ChaosPandion; 26.05.2011
comment
Я только что попытался зарегистрироваться, выполнив ClientEntityCache.EntityCacheCleared += ClientEntityCache_CacheCleared; и отменить регистрацию, выполнив ClientEntityCache.EntityCacheCleared -= ClientEntityCache_CacheCleared; вместо вызова нового EntityCacheClearedEventHandler, но у меня возникла та же проблема :(. - person Carl; 26.05.2011
comment
@Chaos: есть ссылка? Я хочу разобраться в этом... нет смысла это делать... - person IAbstract; 26.05.2011
comment
@IAbstract, используя ILDASM, вы увидите, что компилятор генерирует один и тот же IL для любого синтаксиса. Компилятор выдает код для создания экземпляра делегата, если он не был указан явно, поэтому независимо от используемого синтаксиса C# конечным результатом будет ClientEntityCache.EntityCacheCleared += new MyDelegate(ClientEntityCache_CacheCleared); - person Chris Taylor; 26.05.2011
comment
@IAbstract, нет проблем. Это распространенное заблуждение, и, честно говоря, делегаты и, следовательно, события, представленные в этом отношении неинтуитивно, определенно можно было бы ожидать, что вы должны удалить ту же ссылку, даже способ обработки null не очень интуитивно понятен и часто является предметом обсуждения. - person Chris Taylor; 26.05.2011