WeakEventManager 和 Destructor 的内存保留问题

Ali*_*son 5 c# weakeventmanager

我遇到了与 WeakEventManager 的使用相关的内存保留问题。使用 DotMemoryProfiler 对我的应用程序进行分析后,我发现每当我使用以下命令添加处理程序时WeakEventManager.AddHandler,都会导致向 ConditionalWeakTable<object, object> 添加一个实例,并且该条目将被保留,除非手动删除该处理程序(即使在析构函数中删除也不会\ xe2\x80\x99t 工作,你必须有一个函数来显式删除它)。

\n

从下面的代码中,您可以看到手动调用 Detach 方法与通过析构函数调用它是有区别的。手动调用时,ConditionalWeakTable 会被正确收集并在 DotMemoryProfiler 快照中标记为“Dead”。然而,当从析构函数 ( ) 调用它时~Subscriber,ConditionalWeakTable 仍然存在。请注意,快照是在执行 Detach 之后拍摄的(因为序列打印在控制台中)。我还确保它在发布模式下运行,并且未选中“抑制 JIT 优化”选项

\n

这是显示幸存对象的 DotMemory 快照的比较:

\n

在此输入图像描述

\n

您可能认为幸存的字节不多\xe2\x80\x99,但这在我们的实际应用程序中引起了问题,因为我们广泛使用 Wea​​kEventManager 并通过析构函数取消订阅它,导致保留大量内存(超过12MB)。\n这是一个示例的屏幕截图,它可以在我们的实际应用程序中保留如此巨大的内存量。

\n

在此输入图像描述

\n

为了提供更多上下文,这里是相关的代码片段:

\n
class Program\n{\n    static void Main()\n    {\n        var isolator = new Action(() => { \n            var publisher = new Publisher();\n            var subscriber = new Subscriber();\n\n            subscriber.Init(publisher);\n            MemoryProfiler.GetSnapshot();\n\n            //subscriber.Detach(publisher); // detach it manually won\'t cause issue\n      });\n            isolator();\n\n        for (int i = 0; i < 10; i++)\n        {\n            GC.Collect();\n            GC.WaitForPendingFinalizers();\n        }\n\n        MemoryProfiler.GetSnapshot();\n        Console.WriteLine("2nd snapshot captured");\n\n    }\n\n    private static void EventHandlerMethod(object sender, EventArgs e) { }\n}\n\npublic class Publisher : INotifyPropertyChanged\n{\n    private string name;\n    public string Name\n    {\n        get => name;\n        set\n        {\n            name = value;\n            OnPropertyChanged(nameof(Name));\n        }\n    }\n\n    public event PropertyChangedEventHandler PropertyChanged;\n\n    protected virtual void OnPropertyChanged(string propertyName) => \n        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));\n}\n\npublic class Subscriber : IWeakEventListener\n{\n    Publisher _publisher;\n    public void Init(Publisher publisher)\n    {\n        _publisher = publisher;\n        WeakEventManager<Publisher, PropertyChangedEventArgs>.AddHandler(publisher, "PropertyChanged", OnPublisherPropertyChanged);\n    }\n\n    public void Detach(Publisher publisher)\n    {\n        WeakEventManager<Publisher, PropertyChangedEventArgs>.RemoveHandler(publisher, "PropertyChanged", OnPublisherPropertyChanged);\n        Console.WriteLine("Detach called");\n    }\n\n    ~Subscriber()\n    {\n        Console.WriteLine("Destructor called");\n        Detach(_publisher);\n    }\n\n    private void OnPublisherPropertyChanged(object sender, PropertyChangedEventArgs e) { }\n\n    public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e)\n    {\n        return true;\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这是两种情况的控制台输出:

\n
    \n
  • 通过析构函数调用 Detach
  • \n
\n
    Destructor called\n    Detach called\n    2nd snapshot captured\n
Run Code Online (Sandbox Code Playgroud)\n
    \n
  • 禁用析构函数并手动调用 Detach
  • \n
\n
    Detach called\n    2nd snapshot captured\n
Run Code Online (Sandbox Code Playgroud)\n

请注意,在这两种情况下,都会调用 Detach 方法。是的,它被称为!但是,如果我们从析构函数中调用RemoveHandler,为什么它不能\xe2\x80\x99 工作呢?

\n

我的问题:

\n
    \n
  1. 为什么在通过析构函数调用 ConditionalWeakTable 时,即使在删除事件处理程序/GC 后,ConditionalWeakTable 仍会保留内存?
  2. \n
  3. 如何解决有效清除它?
  4. \n
\n

小智 2

WeakEventManager被设计为在独占线程中工作。例如,在 UI 线程中注册处理程序。

此类的内部实现将为每个托管线程创建一个新实例(由于用ThreadStatic属性标记的静态字段)。

因此,为了正常工作WeakEventManager,有必要确保对该类的方法的每次调用都来自同一个线程。

最简单的方法是使用Dispatcher类的实例,该实例将在专用线程中运行(就像我们在任何 UI 应用程序中想要与 UI 线程同步时所做的那样)。

ThreadPool.QueueUserWorkItem(_ =>
{
    _main = Dispatcher.CurrentDispatcher;
    Dispatcher.Run();
});
Run Code Online (Sandbox Code Playgroud)

之后,我们可以在代码中的任何地方与该线程同步

_main.Invoke(() =>
{
    var publisher = new Publisher();
    var subscriber = new Subscriber();
    subscriber.Init(publisher);
    Console.WriteLine("isolator completed";)
});
Run Code Online (Sandbox Code Playgroud)

使用这种用法,无需在析构函数中取消订阅事件,因为内部方法DoCleanup将为我们执行此操作,该方法会扫描所有用WeakReference.

你的情况。在代码中,在 thread 中订阅了一个事件1,但在 thread 中取消了订阅2。也就是说,您正在订阅 的另一个实例WeakEventManager,但这只是问题的一半。主要问题是您没有为您使用的线程运行调度程序WeakEventManager。开始删除已收集的处理程序是GC通过调度程序完成的,如果不运行调度程序,您将遇到内存泄漏。在添加新侦听器后或在触发事件时未找到有效的处理程序时启动该过程