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

时间:2015-08-21 10:13:02

标签: c# wpf multithreading

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

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

var temp = new DrawingGroup();

如果你经常在不同的后台(非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);
        }
    }
}

在大约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 ...

我认为发生的事情是:

  1. DrawingGroup派生自DependencyObjectDependencyObject的构造函数使用Dispatcher.CurrentDispatcher,然后为此主题创建新的Dispatcher
  2. 新调度程序分配一些Win32资源。
  3. 在Reflector中查看HwndWrapper的终结代码,我认为HwndWrapper会尝试使用Dispatcher.BeginInvoke同步自己的清理。
  4. 由于此后台线程永远不会启动消息循环,因此清理代码永远不会被调用=&gt;资源泄漏
  5. 有没有办法解决或解决这个问题?

    到目前为止我尝试过:

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

2 个答案:

答案 0 :(得分:1)

我是否正确,您使用TPLThreadPool运行此逻辑? 如果是这样,最后一个案例是您的选项,您可以轻松获取DrawingGroup Dispatcher属性,并在InvokeShutdown方法中调用它http://www.nerdparadise.com/tech/python/pygame/basics/part3/ {1}}阻止。

所以你可以这样写:

finally

答案 1 :(得分:1)

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

它与使用冷冻操作或任何操作无关。从DependencyObject派生的任何对象(类)都将具有 基础构造函数 ,用于触发为该线程创建Dispatcher实例,如果之前没有创建过。换句话说,Dispatcher是一个线程局部单例设计。

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

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

Thread currentThread = Thread.CurrentThread;
Dispatcher currentDispatcherOrNull = Dispatcher.FromThread(currentThread);

MSDN: Dispatcher.FromThread method

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

MSDN: Dispatcher.InvokeShutdown method

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

结合第一点和第二点可以得出结论:您需要自己的线程池,每个线程池都赋予Dispatcher。只要您控制线程池的风向下,就没有泄漏的危险。

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

如果您控制前端(表示层)和后端(图像渲染),则 更简单,更严格,更有效 (虽然利用不足)方法:

  • 使调度程序必须初始化Dispatcher的策略;后端只会检查调度程序 是否已存在 (通过Dispatcher.FromThread), 拒绝执行 如果不是

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

此方法也适用于 一个 的线程池。