【问题标题】:Resource leak when creating freezable objects in a background thread在后台线程中创建可冻结对象时资源泄漏
【发布时间】:2015-08-21 10:13:02
【问题描述】:

在我的应用程序中,我在后台(线程池)线程中创建 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. 由于此后台线程从不启动消息循环,因此将永远不会调用清理代码 => 资源泄漏

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

到目前为止我所尝试的:

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

【问题讨论】:

    标签: c# wpf multithreading


    【解决方案1】:

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

    它与使用冻结操作或任何操作无关。任何从DependencyObject 派生的对象(类)都将有一个基础构造函数,它会触发为该线程创建Dispatcher 实例(如果它没有) t 之前创建的。换句话说,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。只要控制好线程池的wind down,就没有泄露的危险。


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


    如果您同时控制前端(表示层)和后端(图像渲染),则有一个更简单、更严格、更有效 (尽管未充分利用)方法:

    • 使调用者必须初始化 Dispatcher 成为一项策略;后端只会检查调度程序已经存在(通过Dispatcher.FromThread),并且拒绝工作 如果不是。

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

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

    【讨论】:

    • 感谢您的详细回答!旁注:我的应用程序的生命周期是无限期的——它一年 365 天、一天 24 小时运行。所以即使线程池只是偶尔创建新线程,最终它也会崩溃,这就是为什么这对我来说是个问题。您是否有第一段中“已知错误/不会被修复”的声明的来源?是否有您个人推荐的操作系统线程池替代方案?
    【解决方案2】:

    您使用TPLThreadPool 运行此逻辑是否正确?
    如果是这样,并且您可以选择最后一种情况,您可以轻松获取DrawingGroupDispatcher 属性,并在finally 块中调用它的InvokeShutdown 方法。

    所以你可以这样写:

    DrawingGroup temp;
    try
    {
        temp = new DrawingGroup();
    }
    finally
    {
        // do the work
        if (temp != null)
        {
            temp.Dispatcher.InvokeShutdown();
        }
    }
    

    【讨论】:

    • +1 好主意,但我不确定它是否“合法”:ThreadPool 线程可能会再次执行相同的代码,但它不会(总是)创建一个新的Dispatcher - 它可能使用“已处置”的实例。我不确定DrawingGroup(或任何Freezable)需要Dispatcher 来做什么,但将它与半清理的Dispatcher 一起使用不是很危险吗?
    • 您应该在不需要DrawingGroup 的那一刻清除Dispatcher。我确信在其中的constructor 中有一个检查Dispatcher 的逻辑,所以我在这里看不到任何伤害。尝试在referencesource.microsoft.com 上查看它的代码
    • 不,我担心我在同一个线程上创建的 next DrawingGroup。那个会被毁掉一半的Dispatcher。在参考源中查看对DispatcherObject.Dispatcher 的每次访问都比我想象的要多一些工作......
    • 我认为 next DrawingGroup 将创建一个新的 Dispatcher,如果旧的 Dispatcher 已关闭,但现在无法证明我的话。
    • 不,不会,这就是我一直想说的。我已经检查了源代码和调试器。如果您在InvokeShutdown 之后创建一个新的DrawingGroup,则新的DG 将获得相同的半毁坏Dispatcher 对象,处于完全相同的状态InvokeShutdown 离开它。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-09-01
    • 2013-02-21
    • 2014-02-26
    • 1970-01-01
    • 2014-02-10
    • 1970-01-01
    相关资源
    最近更新 更多