在后台线程中创建可冻结对象时资源泄漏 [英] Resource leak when creating freezable objects in a background thread

查看:57
本文介绍了在后台线程中创建可冻结对象时资源泄漏的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

In my application, I create Freezable objects in background (thread pool) threads, freeze them and later display them on the main thread. Everything works, except after some time, the whole system becomes sluggish and the application eventually crashes.

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

I have managed to reduce the problem to this line:

var temp = new DrawingGroup();

如果您经常在不同的后台(非UI)线程上运行足够的时间,则整个系统将变得缓慢,并最终导致应用程序崩溃.

If you run that often enough on different background (non-UI) threads, the whole system becomes sluggish, and eventually the application crashes.

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

(In my real application, I then draw something to this object, freeze it and later display it on the main thread, but that's not necessary to reproduce the problem.)

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

Full code to reproduce that problem (copy into a default blank wpf application):

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个线程之后),我得到了以下异常:

After ~150 timer runs (i.e. after about 15000 threads were created for a short time), I get this exception:

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是从DependencyObject派生的,并且DependencyObject的构造函数使用Dispatcher.CurrentDispatcher,然后为该线程创建一个新的Dispatcher.
  2. 新调度程序分配一些Win32资源.
  3. 看看Reflector中HwndWrapper的完成代码,我认为HwndWrapper尝试使用Dispatcher.BeginInvoke同步其自身的清理.
  4. 由于此后台线程永远不会启动消息循环,因此清理代码将永远不会被调用=>资源泄漏
  1. DrawingGroup is derived from DependencyObject, and DependencyObject's constructor uses Dispatcher.CurrentDispatcher, which then creates a new Dispatcher for this thread.
  2. The new dispatcher allocates some Win32 resource.
  3. Looking at HwndWrapper's finalization code in Reflector, I think HwndWrapper tries to synchronize it's own cleanup using Dispatcher.BeginInvoke.
  4. Since this background thread never starts a message loop, the cleanup code will never get called => Resource leak

是否有解决或解决此问题的方法?

Is there a way to solve or work around this problem?

到目前为止我尝试过的事情:

What I have tried so far:

  • 显然,使用ThreadPoolTasks而不是手动创建线程会延迟此问题.但是ThreadPool也会随着时间的推移创建和关闭新线程,因此只能延迟问题,这不是解决方案.
  • 在每个线程结束时强制进行完整的GC收集并没有改变任何内容.这与垃圾回收不确定性有关.
  • 在后台线程末尾手动调用Dispatcher.InvokeShutdown似乎可行,但是我看不到如何确保在每个ThreadPool线程末尾调用它.不用写我自己的ThreadPool,就是...
  • Obviously, using the ThreadPool or Tasks instead of creating threads by hand delays this problem. But the ThreadPool creates and shuts down new threads over time, too, so that only delays the problem, it's not a solution.
  • Forcing a full GC collect at the end of each thread didn't change anything. This isn't about garbage collection indeterminism.
  • Calling Dispatcher.InvokeShutdown manually at the end of the background thread seems to work, but I don't see how I could ensure it gets invoked at the end of every ThreadPool thread. Without writing my own ThreadPool, that is...

推荐答案

这是.NET中Dispatcher系统设计的一个已知缺陷.它会影响依赖于Dispatcher的WPF和非WPF库. Microsoft已经表示这不会得到解决.

This is a known defect of the Dispatcher system design in .NET. It affects WPF and non-WPF libraries that relies on Dispatcher. Microsoft has indicated that this will not be fixed.

它与冻结操作或任何其他操作无关.从DependencyObject派生的任何对象(类)都将具有 基本构造函数 ,如果该线程尚未创建,则触发该线程的Dispatcher实例的创建.之前已创建.换句话说,Dispatcher在设计上是线程本地的单例.

It is not tied to the use of freezing operation, or any operation whatsoever. Any object (class) that is derived from DependencyObject, will have a base constructor that triggers the creation of the Dispatcher instance for that thread, if it hasn't been created before. In other words, Dispatcher is a thread-local singleton by design.

当泄漏了足够(数以万计)的Dispatcher实例时,就会发生崩溃.这意味着在应用程序的生存期内已创建和销毁了相同数量的线程,每个线程都创建了一个或多个DependencyObject.询问任何应用程序开发人员,他们会说 不常见,虽然本身并不坏,但绝对需要特别注意 ,以设计具有许多线程的应用程序.已被创建并销毁.

The crash happens when enough (tens of thousands) of instances of Dispatcher have been leaked. This implies that the same number of threads had been created and destroyed through the application's lifetime, each of which having created one or more DependencyObject's. Ask any application developer and they'll say that it would be uncommon, though not bad per se, but definitely needs some special care to design an application that have many threads to have been created and destroyed.

在开始之前,这是一种安全的方法来查询Dispatcher ,而不会导致以前没有的自动创建 .

Before beginning, here is a safe approach to query the Dispatcher, without causing one to be automatically created if it didn't exist before.

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

MSDN :Dispatcher.FromThread方法

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

Firstly, you can shutdown the dispatcher after you're done with the thread.

MSDN :Dispatcher.InvokeShutdown方法

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

Secondly, realize that once it has been shut down for one thread, it is impossible to re-initialize Dispatcher for that same thread. In other words, after the InvokeShutdown, it is not possible to use WPF or any other library dependent on Dispatcher on that thread. The thread is effectively poisoned to death.

结合第一点和第二点,可以得出结论,您需要自己的线程池,每个线程池都有一个Dispatcher.只要您控制线程池的关闭,就不会有泄漏的危险.

Combining the first and second points leads one to conclude that you need your own thread pool, each of which is endowed with a Dispatcher. As long as you are in control of the thread pool's wind down, there is no danger of leaking.

有流行的开源.NET线程池库,它们可以与.NET系统线程池并列运行.这是解决此特定平台问题的合适方法.

There are popular open-source .NET thread pool libraries which can run alongside (independently of) the .NET system thread pool. It is an appropriate way of solving this particular platform issue.

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

If you are in control of both the front-end (presentation layer) and back-end (image rendering), there is a simpler, draconian, and effective (though under-utilizing) approach:

  • 使调用者必须初始化Dispatcher的策略;后端将仅检查调度员 已经存在 (通过Dispatcher.FromThread),并且 拒绝工作 否则.

这种方法将负担转移到表示层,具有讽刺意味的是,这种分配趋向于使Dispatcher已经初始化.

This approach shifts the burden to the presentation layer, which ironically tends to have the Dispatcher already initialized.

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

This approach is also applicable for a thread pool of one.

这篇关于在后台线程中创建可冻结对象时资源泄漏的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆