为什么使用JpegBitmapDecoder的LongRunning任务(TPL)资源耗尽?

ale*_*exg 12 .net c# task-parallel-library

我们有一个托管的.Net/C#应用程序,可以创建TPL任务来对JPEG图像执行JPEG元数据编码.每个任务都使用TaskCreationOptions.LongRunning选项构建,例如,

Task task = new Task( () => TaskProc(), cancelToken, TaskCreationOptions.LongRunning );
Run Code Online (Sandbox Code Playgroud)

TaskProc()利用JpegBitmapDecoder和JpegBitmapEncoder类添加JPEG元数据并将新图像保存到磁盘.我们允许在任何时间最多激活2个此类任务,此过程应无限期地继续.

执行上述操作一段时间后,我们得到了足够的存储空间可用于在尝试创建JpegBitmapDecoder类的实例时处理此命令异常:

System.ComponentModel.Win32Exception(0x80004005的):没有足够的存储可在MS.Win32.UnsafeNativeMethods.RegisterClassEx(WNDCLASSEX_D wc_d)处理此命令
在MS.Win32.HwndWrapper..ctor(的Int32 classStyle,的Int32风格的Int32扩展风格,INT3 2×,的Int32 Y,的Int32宽度,高度的Int32,字符串名称,IntPtr的父,HwndWrapperHoo K []钩子)在System.Windows.Threading.Dispatcher..ctor()在System.Windows.Threading.Dispatcher.get_CurrentDispatcher()在System.Windows.Media.Imaging.BitmapDecoder..ctor(流bitmapStream,BitmapC reateOptions createOptions,BitmapCacheOption cacheOption,的Guid expectedClsId)在System.Windows.Media.Imaging.JpegBitmapDecoder..ctor(流bitmapStream,位mapCreateOptions createOptions,BitmapCacheOption cacheOption)

当我们使用JpegBitmapDecoder添加元数据时才会发生错误.换句话说,如果任务只是编码并将Bitmap图像保存到文件,则不会出现任何问题.使用Process Explorer,Process Monitor或其他诊断工具时,没有发现任何明显的事实.根本没有观察到线程,内存或手柄泄漏.发生此类错误时,不会启动任何新应用程序,例如记事本,文字等.一旦我们的应用程序终止,一切都会恢复正常.

LongRunning的任务创建选项在MSDN中定义为指定任务将是长时间运行的粗粒度操作.它向TaskScheduler提供了一个提示,即可以保证超额订阅.这意味着选择运行任务的线程可能不是来自ThreadPool,即,它将为任务的目的而创建.其他任务创建选项将导致为任务选择ThreadPool线程.

经过一段时间的分析和测试,我们将任务创建选项更改为除LongRunning之外的任何其他选项,例如PreferFairness.根本没有对代码进行任何其他更改.这"解决了"问题,即不再耗尽存储错误.

我们对LongRunning线程成为罪魁祸首的实际原因感到困惑.以下是我们对此的一些问题:

  1. 为什么选择执行任务的线程来自ThreadPool?如果线程终止,那么GC的资源是否应该随时间回收并返回操作系统,无论其来源如何?

  2. LongRunning任务和导致错误的JpegBitmapDecoder功能的组合有什么特别之处?

Dre*_*rsh 14

在类System.Windows.Media.Imaging的命名空间是基于Dispatcher线程架构.对于更好或更坏的部分默认行为是,Dispatcher当某个组件通过静态Dispatcher.Current属性请求当前调度程序时,在任何正在执行的线程上启动新的.这意味着为线程启动了整个Dispatcher"运行时",并且分配了所有类型的资源,如果没有正确清理,将导致管理泄漏.在Dispatcher"运行"还预计,其上执行的线程是STA线程标准的消息抽水正在进行和Task运行时,默认情况下,没有启动STA线程.

所以,所有这一切,为什么它会发生在LongRunning而不是一个"常规"ThreadPool线程?原因LongRunning意味着您每次都会启动一个新线程,这意味着每次都会有新的Dispatcher资源.最终,如果让默认任务调度程序(基于ThreadPool的程序)运行得足够长,它也会耗尽空间,因为没有任何东西可以为Dispatcher运行时提供消息,以便能够清理它所需要的东西.

因此,如果你想使用Dispatcher像这样的基于线程的类,你真的需要使用一个自定义TaskScheduler,这个自定义旨在在Dispatcher正确管理"运行时" 的线程池上运行这种工作.好消息是你运气好,因为我已经写了一个你可以抓到的.FWIW,我在三个非常大量的生产代码中使用这个实现,每天处理数十万个图像.

实施更新

我最近再次更新了实现,因此它与async.NET 4.5 的新功能兼容.最初的实施与SynchronizationContext概念没有合作,因为它不一定是.既然您可能await在Dispatcher线程上执行的方法中使用C#中的关键字,我需要能够与之合作.以前的实现会在这种情况下陷入僵局,这个最新的实现没有.

  • 我会考虑到这一点,因为它让我有些头疼.许多System.Windows.Media.Imaging类不实现IDisposable,但保留对非托管资源的引用.这些类继承自`CriticalFinalizerObject`,因此,当收集垃圾时,进入终结队列,该队列由终结线程清理,终结线程的运行优先级低于用户线程.给定一个受CPU限制的应用程序,该终结队列会变得饱和,并且您将耗尽非托管资源. (3认同)

pat*_*ter 5

我可以自己重现并修复这个问题,同时从Uri构造BitmapSource对象.与您一样,只有在TaskCreationOptions.LongRunning时才会出现.

为了避免在这种特殊情况下泄漏,我发现您可以在实例化所需的WPF对象后立即关闭Dispatcher.

这是我的TaskProc的工作实现:

private static BitmapImage TaskProc()
{
    var result = new BitmapImage(new Uri(@"c:\test.jpg"));
    // the following line fixes the problem, no more leaks occur
    result.Dispatcher.InvokeShutdown();
    return result;
}
Run Code Online (Sandbox Code Playgroud)