Подтвердить что ты не робот

Почему задача LongRunning (TPL) с JpegBitmapDecoder заканчивается из ресурсов?

У нас есть управляемое приложение .Net/С#, которое создает задачи TPL для выполнения кодирования метаданных JPEG на изображениях JPEG. Каждая задача строится с параметром TaskCreationOptions.LongRunning, например,

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

TaskProc() использует классы JpegBitmapDecoder и JpegBitmapEncoder для добавления метаданных JPEG и сохранения новых изображений на диск. Мы позволяем до двух таких задач быть активными в любой момент времени, и этот процесс должен продолжаться бесконечно.

После некоторого времени выполнения вышеупомянутого мы получаем Недостаточно свободного места для хранения для обработки этой исключительной команды при попытке создать экземпляр класса 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 [] hooks) в System.Windows.Threading.Dispatcher..ctor() at System.Windows.Threading.Dispatcher.get_CurrentDispatcher() в System.Windows.Media.Imaging.BitmapDecoder..ctor(Stream bitmapStream, BitmapC reateOptions createOptions, BitmapCacheOption cacheOption, Guid expectedClsId) в System.Windows.Media.Imaging.JpegBitmapDecoder..ctor(Stream bitmapStream, бит mapCreateOptions createOptions, BitmapCacheOption cacheOption)

Произошла ошибка только, когда мы использовали JpegBitmapDecoder для добавления метаданных. Другими словами, если задача просто закодирует и сохранит изображение Bitmap в файл, никаких проблем не возникнет. Ничего очевидного не было обнаружено при использовании Process Explorer, Process Monitor или других диагностических инструментов. Никакой поток, память или утечка ручек не наблюдались вообще. При возникновении такой ошибки новые приложения не могут быть запущены, например, блокнот, слово и т.д. Как только наше приложение будет прекращено, все вернется к нормальному состоянию.

Опция создания задачи LongRunning определена в MSDN как Указывает, что задача будет длительной, крупнозернистой. Он дает подсказку TaskScheduler о том, что избыточная подписка может быть оправдана. Это означает, что поток, выбранный для запуска задачи, может быть не из ThreadPool, т.е. Он будет создан для цели задачи. Другие параметры создания задачи приведут к тому, что для задачи будет выбран поток ThreadPool.

После некоторого анализа времени и тестирования мы изменили способ создания задачи на что угодно, кроме LongRunning, например, PreferFairness. Никаких других изменений кода не было сделано вообще. Это "разрешило" проблему, то есть больше не закончилось ошибок в хранилище.

Мы озадачены фактической причиной того, что потоки LongRunning являются виновниками. Вот некоторые из наших вопросов по этому поводу:

  • Почему должен произойти тот факт, что потоки, выбранные для выполнения задачи, происходят из ThreadPool или нет? Если поток завершается, не должны ли его ресурсы восстанавливаться со временем GC и возвращаться обратно в ОС, независимо от его происхождения?

  • Что особенно важно в сочетании задачи LongRunning и функции JpegBitmapDecoder, которая вызывает ошибку?

4b9b3361

Ответ 1

Классы в пространстве имен System.Windows.Media.Imaging основаны на архитектуре потоков Dispatcher. Для лучшей или худшей части поведения по умолчанию необходимо запустить новый Dispatcher для любого потока, выполняемого всякий раз, когда какой-либо компонент запрашивает текущий диспетчер через статическое свойство Dispatcher.Current. Это означает, что весь поток "Диспетчер времени" запущен для потока, и все виды ресурсов распределяются и, если не правильно очищены, приведут к управляемым утечкам. Время выполнения Dispatcher также ожидает, что поток, который он выполняет, будет потоком STA со стандартным потоком сообщений, а время выполнения Task по умолчанию не запускает потоки STA.

Итак, все, что было сказано, почему это происходит с LongRunning, а не с "регулярным" потоком ThreadPool? Причина LongRunning означает, что вы разворачиваете новый поток каждый раз, что означает новые ресурсы Диспетчера каждый раз. В конце концов, если вы позволите планировщику заданий по умолчанию (основанный на ThreadPool) работать достаточно долго, у него тоже закончится свободное пространство, потому что ничто не перекачивает сообщения для среды выполнения Dispatcher, чтобы иметь возможность очищать все, что ему нужно.

Поэтому, если вы хотите использовать классы, основанные на Dispatcher -thread, как это, вам действительно нужно сделать это с помощью настраиваемого TaskScheduler, который предназначен для выполнения такой работы в пуле потоков, которые управляют Dispatcher "время выполнения". Хорошая новость заключается в том, что вам повезло, потому что я уже написал тот, который вы можете захватить здесь. FWIW, я использую эту реализацию в трех очень больших частях производственного кода, которые обрабатывают сотни тысяч изображений в день.

Обновление реализации

Недавно я обновил реализацию, так что она совместима с новыми функциями async.NET 4.5. Первоначальная реализация не согласуется с концепцией SynchronizationContext, потому что это не обязательно. Теперь, когда вы используете ключевое слово await в С# в методе, выполняющемся в потоке диспетчера, мне нужно иметь возможность сотрудничать с ним. Предыдущая реализация была бы тупиковой в этой ситуации, эта последняя реализация не делает.

Ответ 2

Я могу воспроизвести и исправить эту проблему самостоятельно, создав объекты BitmapSource из Uri. Как и с вами, это происходит только в том случае, если TaskCreationOptions.LongRunning.

Чтобы избежать утечки в этой конкретной ситуации, я обнаружил, что вы можете закрыть Диспетчер, как только вы создадите объект WPF, который вам нужен.

Здесь моя рабочая реализация 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;
}