C# 在取消挂钩后仍然挂钩于某个事件

发布于 2024-11-09 21:46:03 字数 1286 浏览 1 评论 0原文

我目前正在调试一个包含内存泄漏的大型(非常大!)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
}

编辑

这是我用于订阅的代码:

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

我还更改了取消注册的代码,以不调用新的 EntityCacheClearedEventHandler。

编辑结束

我在使用 WorkItem 的代码中的正确位置调用了 Dispose,当我调试时,我可以看到该函数确实被调用,并且我为每个事件执行 -= 。但我仍然遇到内存泄漏,并且我的 WorkItems 在被处理后仍然保持活动状态,并且在 .NET 探查器中我可以看到实例保持活动状态,因为事件处理程序(如 EntityCacheClearedEventHandler)仍然将它们保留在其调用列表中。我多次尝试将它们脱钩(多次-=),只是为了确保它们不会多次被钩住,但这没有帮助。

任何人都知道为什么会发生这种情况或者我可以采取什么措施来解决这个问题? 我想我可以更改事件处理程序以使用弱委托,但这需要大量处理遗留代码。

谢谢!

编辑:

如果这有帮助,这里是 .NET 探查器描述的根路径: 很多东西都指向ClientEntityCache,它指向EntityCacheClearedEventHandler,它指向Object[],它指向EntityCacheClearedEventHandler的另一个实例(我不明白为什么),它指向WorkItem。

I am currently debugging a big (very big!) C# application that contains memory leaks. It mainly uses Winforms for the GUI, though a couple of controls are made in WPF and hosted with an ElementHost. Until now, I have found that many of the memory leaks were caused by events not being unhooked (by calling -=) and I've been able to solve the problem.

However, I just came across a similar problem. There is a class called WorkItem (short lived) which in the constructor registers to events of another class called ClientEntityCache (long lived). The events were never unhooked and I could see in .NET profiler that instances of WorkItem were being kept alive when they shouldn't because of those events. So I decided to make WorkItem implement IDisposable and in the Dispose() function I unhook the events this way:

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

EDIT

Here is the code I use for subscription:

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

I also changed the code for unregistering to not call new EntityCacheClearedEventHandler.

END OF EDIT

I made the calls to Dispose at the proper places in the code that uses WorkItem and when I debug I can see that the function is really being called and I do -= for every event. But I still get a memory leak and my WorkItems still stay alive after being Disposed and in .NET profiler I can see that the instances are kept alive because the event handlers (like EntityCacheClearedEventHandler) still have them in their invocation list. I tried to unhook them more than once (multiple -=) just to make sure they were not hooked more than once but this doesn't help.

Anyone has an idea why this is happening or what I could do to solve the problem?
I suppose I could change the event handlers to use weak delegates but this would require to mess a lot with a big pile of legacy code.

Thanks!

EDIT:

If this helps, here is the root path described by .NET profiler:
lots of things point on ClientEntityCache, which points to EntityCacheClearedEventHandler, which points to Object[], which points to another instance of EntityCacheClearedEventHandler (I don't understand why), which points to WorkItem.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

晨曦÷微暖 2024-11-16 21:46:03

可能有多个不同的委托函数连接到该事件。希望下面的小例子能让我更清楚地理解我的意思。

// 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 的委托之前,事件仍将引用它。

在此处输入图像描述

It might be that multiple different delegate functions are wired to the event. Hopefully the following little example will make it clearer as to what I mean.

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

If you break before the removal of the event handler you can view the invocation list in the debugger. See below, there are 2 handlers, one for MyEventHandler1 and another for the method MyEventHandler2.

enter image description here

Now after removing the MyEventHandler1 twice, MyEventHandler2 is still registered, because there is only one delegate left it looks a little different, it is no longer showing in the list, but until the delegate for MyEventHandler2 is removed it will still be referenced by the event.

enter image description here

2024-11-16 21:46:03

取消挂钩事件时,它需要是同一个委托。像这样:

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

因此 -= 不会取消您订阅的原始委托,因为它们是不同的委托。

When unhooking an event, it needs to be the same delegate. Like this:

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

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

The reason is, what you are using is syntactic sugar for this:

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

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

So the -= doesn't unhook the original one you subscribed with because they are different delegates.

丢了幸福的猪 2024-11-16 21:46:03

您是否取消了正确的参考?当您使用 -= 取消挂钩时,不会产生任何错误,并且如果您取消未挂钩的事件,则不会发生任何情况。但是,如果您使用 += 添加,并且事件已被挂钩,您将收到错误消息。现在,这只是您诊断问题的一种方法,但请尝试添加事件,如果您没有收到错误,则问题是您使用错误的引用取消了事件。

Are you unhooking the right reference? When you unhook using -= no error is produced and if you're unhooking events which aren't hooked nothing will happen. However if you add using += you'll get an error if the event is already hooked. Now, this is only a way for you to diagnose the problem but try adding the events instead and if you DONT get an error the problem is that your unhooking the event with the wrong reference.

最佳男配角 2024-11-16 21:46:03

如果事件处理程序使实例保持活动状态,则 GC 不会调用 Dispose,因为事件源仍在引用它。

如果您自己调用 Dispose 方法,则引用将超出范围。

Dispose won't get called by the GC if the instance is being kept alive by the event handlers, as it is still being referenced by the source of the events.

If you called your Dispose method yourself, the references would then go out of scope.

妄断弥空 2024-11-16 21:46:03

也许可以尝试:

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

您正在创建一个新的事件处理程序并将其从委托中删除 - 这实际上什么也不做。

通过删除对原始订阅事件方法的引用来删除事件订阅。

您始终可以只设置 eventhandler = delegate {}; 在我看来,这比 null 更好。

Maybe try:

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

You are creating a new event handler and removing it from the delegate - which effectively does nothing.

Remove the event subscription by removing reference to the original subscribing event method.

You could always just set your eventhandler = delegate {}; In my opinion, that would be better than null.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文