C # все еще подключен к событию после отсоединения - PullRequest
7 голосов
/ 26 мая 2011

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

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

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

EDIT

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

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

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

Конец редактирования

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

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

Спасибо!

EDIT:

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

Ответы [ 5 ]

4 голосов
/ 26 мая 2011

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

// 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.

enter image description here

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

enter image description here

2 голосов
/ 26 мая 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);
     }
}

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

0 голосов
/ 26 мая 2011

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

0 голосов
/ 26 мая 2011

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

0 голосов
/ 26 мая 2011

Может быть попробовать:

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

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

Удалить подписку на событие, удалив ссылку на оригинал метод подписки на событие.

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

...