在后台线程中创建可冻结对象时发生资源泄漏

Nik*_*iki 5 c# wpf multithreading

在我的应用程序中,我在后台(线程池)线程中创建Freezable对象,冻结它们,然后在主线程上显示它们。一切正常,除了一段时间后,整个系统变得缓慢并且应用程序最终崩溃。

我已经设法将问题减少到这一行:

var temp = new DrawingGroup();
Run Code Online (Sandbox Code Playgroud)

如果您在不同的后台(非 UI)线程上运行得足够频繁,整个系统就会变得缓慢,最终应用程序崩溃。

(在我的实际应用程序中,然后我向该对象绘制一些内容,冻结它,然后将其显示在主线程上,但这并不是重现问题所必需的。)

重现该问题的完整代码(复制到默认的空白 wpf 应用程序中):

public partial class MainWindow : Window
{
    private DispatcherTimer dt;

    public MainWindow()
    {
        InitializeComponent();

        dt = new DispatcherTimer();
        dt.Interval = TimeSpan.FromSeconds(0.1);
        dt.Tick += dt_Tick;
        dt.IsEnabled = true;
    }

    private int counter = 0;
    void dt_Tick(object sender, EventArgs e)
    {
        for (int i = 0; i < 100; i++)
        {
            var thread = new Thread(MemoryLeakTest);
            thread.Start();
        }

        Title = string.Format("Mem leak test {0}", counter++);

    }

    private void MemoryLeakTest()
    {
        try
        {
            var temp = new DrawingGroup();
            temp.Freeze();
        }
        catch (Exception e)
        {
            dt.IsEnabled = false;
            MessageBox.Show(e.Message+Environment.NewLine+e.StackTrace);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

在大约 150 个计时器运行后(即在短时间内创建了大约 15000 个线程后),我收到此异常:

Not enough storage is available to process this command
   bei MS.Win32.HwndWrapper..ctor(Int32 classStyle, Int32 style, Int32 exStyle, Int32 x, Int32 y, Int32 width, Int32 height, String name, IntPtr parent, HwndWrapperHook[] hooks)
   bei System.Windows.Threading.Dispatcher..ctor()
   bei System.Windows.DependencyObject..ctor()
   bei System.Windows.Media.DrawingGroup..ctor()
   bei WpfApplication5.MainWindow.MemoryLeakTest() in ...
Run Code Online (Sandbox Code Playgroud)

我认为正在发生的事情是这样的:

  1. DrawingGroup派生自DependencyObject, andDependencyObject的构造函数使用Dispatcher.CurrentDispatcher,然后Dispatcher为该线程创建一个新的。
  2. 新的调度程序分配一些 Win32 资源。
  3. 看看HwndWrapperReflector 中的最终化代码,我认为HwndWrapper尝试使用 来同步它自己的清理Dispatcher.BeginInvoke
  4. 由于此后台线程永远不会启动消息循环,因此永远不会调用清理代码 => 资源泄漏

有没有办法解决或解决这个问题?

到目前为止我已经尝试过:

  • 显然,使用ThreadPoolorTasks代替手动创建线程会延迟这个问题。但ThreadPool随着时间的推移,它也会创建和关闭新线程,因此这只会延迟问题,而不是解决方案。
  • 在每个线程结束时强制进行完整的 GC 收集不会改变任何内容。这与垃圾收集的不确定性无关。
  • Dispatcher.InvokeShutdown在后台线程结束时手动调用似乎可行,但我不知道如何确保它在每个ThreadPool线程结束时被调用。如果不写我自己的ThreadPool,那就是......

rwo*_*ong 3

这是 .NET 中 Dispatcher 系统设计的一个已知缺陷。它影响依赖 Dispatcher 的 WPF 和非 WPF 库。微软表示不会修复此问题。

它与冻结操作或任何操作的使用无关。从 派生的任何对象(类)DependencyObject都将具有一个基本构造函数Dispatcher,如果之前尚未创建该实例,则该构造函数会触发该线程的实例的创建。换句话说,Dispatcher它被设计为线程本地单例。

当足够(数万)个实例Dispatcher被泄露时,就会发生崩溃。这意味着在应用程序的生命周期中创建和销毁了相同数量的线程,每个线程都创建了一个或多个DependencyObject。询问任何应用程序开发人员,他们都会说这种情况并不常见,尽管本身并不坏,但在设计一个需要创建和销毁许多线程的应用程序时肯定需要特别小心。


在开始之前,这里有一种安全的方法来查询Dispatcher如果它之前不存在,则不会导致自动创建

Thread currentThread = Thread.CurrentThread;
Dispatcher currentDispatcherOrNull = Dispatcher.FromThread(currentThread);
Run Code Online (Sandbox Code Playgroud)

MSDN:Dispatcher.FromThread方法


首先,您可以在完成线程后关闭调度程序

MSDN:Dispatcher.InvokeShutdown方法


其次,要意识到一旦一个线程被关闭,就不可能Dispatcher为同一线程重新初始化。换句话说,在 之后InvokeShutdown,无法使用 WPF 或依赖于Dispatcher该线程的任何其他库。该线程实际上被毒死了。


结合第一点和第二点可以得出结论:您需要自己的线程池,每个线程池都被赋予一个Dispatcher. 只要您能够控制线程池的停止,就不会有泄漏的危险。


有一些流行的开源 .NET 线程池库,它们可以与 .NET 系统线程池一起(独立)运行。这是解决这个特定平台问题的适当方法。


如果您同时控制前端(表示层)和后端(图像渲染),则有一种更简单、更严格且有效(尽管利用率不足)的方法:

  • 制定策略,调用者必须初始化 Dispatcher;后端只会检查调度程序是否已经存在(通过Dispatcher.FromThread),如果不存在则拒绝执行工作。

这种方法将负担转移到表示层,具有讽刺意味的是,表示层往往已经初始化了调度程序。

这种方法也适用于1 个线程池