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

时间:2012-09-27 15:26:23

标签: c# .net task-parallel-library

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

Task task = new Task( () => TaskProc(), cancelToken, TaskCreationOptions.LongRunning );

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 style,Int32   exStyle,Int3 2 x,Int32 y,Int32 width,Int32 height,String name,   IntPtr父,HwndWrapperHoo k []挂钩)   System.Windows.Threading.Dispatcher..ctor()at   System.Windows.Threading.Dispatcher.get_CurrentDispatcher()at   System.Windows.Media.Imaging.BitmapDecoder..ctor(Stream bitmapStream,   BitmapC reateOptions createOptions,BitmapCacheOption cacheOption,   Guid expectedClsId)at   System.Windows.Media.Imaging.JpegBitmapDecoder..ctor(流   bitmapStream,Bit mapCreateOptions createOptions,BitmapCacheOption   cacheOption)

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

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

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

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

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

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

2 个答案:

答案 0 :(得分:14)

System.Windows.Media.Imaging命名空间中的类基于the Dispatcher threading architecture。对于更好或更坏的部分默认行为,只要某个组件通过静态Dispatcher属性请求当前调度程序,就会在正在执行的任何线程上启动新的Dispatcher.Current。这意味着为线程启动了整个Dispatcher“运行时”,并且分配了所有类型的资源,如果没有正确清理,将导致管理泄漏。 Dispatcher“运行时”还期望其执行的线程是一个标准消息泵送的STA线程,默认情况下Task运行时不启动STA线程。

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

因此,如果你想使用Dispatcher这样的基于线程的类,你真的需要使用一个自定义TaskScheduler来设计在线程池上运行这种工作正确管理Dispatcher“运行时”。好消息是你很幸运,因为我已经写了you can grab here。 FWIW,我在三个非常大量的生产代码中使用这个实现,每天处理数十万个图像。

实施更新

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

答案 1 :(得分: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;
}