【问题标题】:WPF framework using Dispatcher.CurrentDispatcher in background thread, causing memory leakWPF 框架在后台线程中使用 Dispatcher.CurrentDispatcher,导致内存泄漏
【发布时间】:2025-11-30 21:40:01
【问题描述】:

我正在使用 WPF CollectionView 并在后台线程中设置过滤器,因为应用此过滤器需要很长时间。

设置此过滤器会触发 CollectionView 的方法ScheduleMapCleanup()(所以我无法更改 WPF 框架代码)。在这个方法中,使用了 Dispatcher.CurrentDispatcher.BeginInvoke。

但是,因为这是在后台线程中执行的,所以这个Action永远不会执行(这个线程的Dispatcher永远不会启动),导致内存泄漏:Dispatcher保持对CollectionView的引用。

我该如何解决这个问题?在 UI 线程中设置过滤器不是一种选择。

我可以自己启动 Dispatcher 吗?如果是这样,我该怎么做(Dispatcher.Run 停止一切)?

【问题讨论】:

  • 你能添加一些你的代码吗?你是怎么做到的?
  • 没有太多代码可以展示。我只是在做 collectionView.Filter = new Predicate(MyFunction);在后台线程中。这会触发 ScheduleMapCleanup() 方法,您可以在此处找到该方法:referencesource.microsoft.com/PresentationFramework/src/… 使用了 Dispatcher.CurrentDispatcher。哪个没有运行。
  • 尽管您的“在 UI 线程中设置过滤器不是一个选项。”,您尝试过吗?发生了什么?应用过滤器不应真正阻塞 UI。
  • "在 UI 线程中设置过滤器不是一个选项。"为什么? CollectionViewSource 被明确记录为不是线程安全的。
  • @Mitch:设置过滤器属性可能需要几分钟(取决于列表中的项目数和过滤器的复杂性)。因此,在此期间冻结 UI 并不是一个真正的选择。通过将它设置在后台线程中,UI 保持可用,并且我可以向用户显示一个很好的加载指示。

标签: c# wpf multithreading dispatcher


【解决方案1】:

当我需要从后台任务更新 UI 线程上的一些控件和绑定时,我会使用它:

Application.Current.Dispatcher.Invoke(
    DispatcherPriority.Loaded,
    new Action(() => {

        // Code here

    })
);

如果不是这样,您能否更具体地说明您想在 UI 线程上做什么

【讨论】:

  • 我无法更改调用 Dispatcher.CurrentDispatcher 的代码。它是 WPF 框架的一部分。
  • 哦,好吧,我不明白我的问题
【解决方案2】:

从后台线程访问当前调度程序不会为您提供 UI 调度程序,它会为您提供一个用于后台线程的新调度程序。

要么从前台线程调用CurrentDispatcher 并将结果传递给后台线程,要么调用DependencyObject.Dispatcher 以获取窗口或其他控件的调度程序。


编辑:我只是更仔细地阅读了这个问题。由于您不控制调用CurrentDispatcher 的代码,因此它的唯一工作方式是从 UI 线程调用该代码。

【讨论】:

    【解决方案3】:

    明确一点:我的代码中没有使用 Dispatcher.CurrentDispatcher。这是在 WPF 框架代码中使用的,所以我无法更改它。 这段代码在后台线程中执行,因为我在后台线程中设置了过滤器。我在后台线程中设置此属性,因为它可能需要几分钟。在后台线程中设置它可以保持 UI 响应并让我向用户显示加载指示。

    我通过向 Dispatcher 添加 Shutdown 并在后台线程中启动调度程序来修复内存泄漏(由未运行的后台 Dispatcher 保持对 CollectionView 的引用引起):

    //All code below is executed on a background thread
    
    //Line below causes WPF framework to add something to Dispatcher.CurrentDispatcher queue.
    view.Filter = new Predicate<Object>(actionTarget.FilterCallback); 
    
    if (Thread.CurrentThread.IsBackground &&  Dispatcher.CurrentDispatcher != Application.Current.Dispatcher)
    {
        Dispatcher.CurrentDispatcher.BeginInvokeShutdown(DispatcherPriority.Background);
        Dispatcher.Run();
    }
    

    如果后台线程稍后被重用(例如,因为它是一个线程池线程,由 BackgroundWorker 启动),您不能像上面的代码那样使用 BeginInvokeShutdown:关闭的调度程序不能再次启动。在这种情况下,请使用它而不是 BeginInvokeShutdown:

    Dispatcher.CurrentDispatcher.BeginInvoke((Action) delegate() { Dispatcher.ExitAllFrames(); }, DispatcherPriority.Background);
    

    这将确保 Run() 方法返回,但调度程序可以稍后再次启动。

    编辑:正如 Mitch 在下面的评论中提到的,当多个线程可以同时执行 Run() 时要小心。如有必要,请在 Run() 周围添加一个锁。

    【讨论】:

    • 这是一个非常有趣的案例来了解调度程序的使用,谢谢!
    • 这将导致随机异常,因为您已经有效地删除了仅使用一个调度程序提供的同步。
    • @Mitch,第二个调度程序只执行了一个操作,不需要与 UI 线程同步。
    • @Lander,你怎么看?它既枚举又修改HashTable 的内容,这不是线程安全的。见*.com/a/5762512/138200
    • @Mitch,你是对的。但在我的情况下,一次只有一个线程可以设置特定 CollectionView 的过滤器,所以这不会导致问题。不过,我会将警告添加到我的答案中。