【问题标题】:Why does LongRunning task (TPL) with JpegBitmapDecoder run out of resources?为什么使用 JpegBitmapDecoder 的 LongRunning 任务 (TPL) 会耗尽资源?
【发布时间】:2012-09-19 10:40:30
【问题描述】:

我们有一个托管的 .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 类样式,Int32 样式,Int32 exStyle,Int3 2 x,Int32 y,Int32 宽度,Int32 高度,字符串名称, IntPtr 父级,HwndWrapperHook [] 钩子)在 System.Windows.Threading.Dispatcher..ctor() 在 System.Windows.Threading.Dispatcher.get_CurrentDispatcher() 在 System.Windows.Media.Imaging.BitmapDecoder..ctor(流位图流, BitmapC reateOptions createOptions, BitmapCacheOption cacheOption, Guid 预期ClsId)在 System.Windows.Media.Imaging.JpegBitmapDecoder..ctor(流 bitmapStream, Bit mapCreateOptions createOptions, BitmapCacheOption 缓存选项)

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

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

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

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

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

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

【问题讨论】:

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


    【解决方案1】:

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

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

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

    实施更新

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

    【讨论】:

    • 感谢德鲁的回复!只是一个后续问题 - 如果处理 [LongRunning] 任务的每个线程都终止(并且它们肯定在保存图像后立即终止),为什么它们不被运行时清理?
    • 我会权衡一下,因为这让我有些头疼。许多 System.Windows.Media.Imaging 类不实现 IDisposable,但保留对非托管资源的引用。这些类继承自CriticalFinalizerObject,因此,当垃圾收集时,进入终结队列,该队列由终结线程清理,该线程以低于用户线程的优先级运行。给定一个受 CPU 限制的应用程序,该终结队列会饱和,并且您会耗尽非托管资源。
    • 抱歉,我仍然不确定我是否完全理解。 Non-LongRunning 任务导致创建新线程,而其他任务使用 ThreadPool 线程。如果这些 System.Windows.Media.Imaging 类是罪魁祸首并且没有如上所述清理,那么如果我们的任务有任何其他 TaskCreationOption,而不仅仅是 LongRunning,那么不会出现同样的问题吗?非常感谢!
    • 在使用线程池线程的情况下,该池线程将在您第一次对其执行某些工作时使用 Dispatcher 运行时进行初始化。完成后,该线程将返回到池中,下次您在同一个线程上工作时,Dispatcher 运行时已经启动并运行,因此您无需支付每次启动全新运行时的开销。与 LongRunning 相比,您每次都明确地创建一个新线程,这意味着需要为其初始化一个全新的 Dispatcher 运行时。
    • WinDbg+SOS,特别是你会想要使用 !DumpHeap。
    【解决方案2】:

    在从 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;
    }
    

    【讨论】:

    • 我们确认您的解决方案与 LongRunning 任务创建选项一起成功运行!非常感谢:-)
    猜你喜欢
    • 2017-12-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-06-29
    • 2015-10-03
    • 2021-10-06
    • 2011-10-01
    • 2021-09-03
    相关资源
    最近更新 更多