14

我们有一个托管的 .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.HwndWrapper..ctor(Int32 classStyle, Int32 style, Int32 exStyle, Int3 ) 的 MS.Win32.UnsafeNativeMethods.RegisterClassEx(WNDCLASSEX_D wc_d) 没有足够的存储空间来处理此命令2 x,Int32 y,Int32 宽度,Int32 高度,字符串名称,IntPtr 父级,HwndWrapperHook [] 挂钩)在 System.Windows.Threading.Dispatcher..ctor() 在 System.Windows.Threading.Dispatcher.get_CurrentDispatcher() 在System.Windows.Media.Imaging.JpegBitmapDecoder..ctor(Stream bitmapStream, BitmapCreateOptions createOptions, BitmapCacheOption cacheOption) 处的 System.Windows.Media.Imaging.BitmapDecoder..ctor(Stream bitmapStream, BitmapC reateOptions createOptions, BitmapCacheOption cacheOption, Guid expectedClsId)

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

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

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

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

  1. 为什么选择执行任务的线程应该来自线程池?如果线程终止,它的资源是否应该随着时间的推移被 GC 回收并返回给操作系统,而不管其来源如何?

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

4

2 回答 2

18

命名空间中的System.Windows.Media.Imaging基于线程架构。无论好坏,默认行为的一部分是每当某些组件通过静态属性请求当前调度程序时,在任何正在执行的线程上启动一个新的。这意味着为线程启动了整个 Dispatcher “运行时”,并分配了各种资源,如果没有正确清理,将导致托管泄漏。“运行时”还期望其执行的线程是具有标准消息泵送的 STA 线程,并且默认情况下,运行时不启动 STA 线程。DispatcherDispatcherDispatcher.CurrentDispatcherTask

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

因此,如果您想使用Dispatcher这样的基于 -thread 的类,您确实需要使用旨在在正确管理“运行时”TaskScheduler的线程池上运行此类工作的自定义来执行此操作。Dispatcher好消息是你很幸运,因为我已经写了一篇你可以在这里获取的。FWIW,我在每天处理数十万张图像的三个非常大量的生产代码部分中使用了这个实现。

实施更新

我最近再次更新了实现,使其与async.NET 4.5 的新功能兼容。最初的实现与这个概念不合作,SynchronizationContext因为它不是必须的。现在您可能await在 Dispatcher 线程上执行的方法中使用 C# 中的关键字,我需要能够与之合作。在这种情况下,以前的实现会死锁,而最新的实现不会。

于 2012-09-27T19:02:00.507 回答
8

在从 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;
}
于 2013-04-30T11:17:05.740 回答