Невозможно создать больше Dispatcher. Закончился ресурс?

В нашем приложении мы используем PngBitmapEncoder для кодирования и сохранения изображения PNG в отдельном потоке\задаче. После нескольких дней работы приложения мы видим, что Dispatcher не может быть создан из Encoder и выдает ошибку.

Недостаточно памяти для обработки команды

И имеет следующий стек вызовов

System.ComponentModel.Win32Exception (0x80004005): Not enough storage is available to process this command
   at MS.Win32.HwndWrapper..ctor(Int32 classStyle, Int32 style, Int32 exStyle, Int32 x, Int32 y, Int32 width, Int32 height, String name, IntPtr parent, HwndWrapperHook[] hooks)
   at System.Windows.Threading.Dispatcher..ctor()
   at System.Windows.Threading.DispatcherObject..ctor()
   at System.Windows.Media.Imaging.BitmapEncoder..ctor(Boolean isBuiltIn)

Поскольку .Net доступен с открытым исходным кодом, стало любопытно, какая строка внутри конструктора Dispatcher выдает ошибку.

    [SecurityCritical, SecurityTreatAsSafe]
    private Dispatcher()
    {
        _queue = new PriorityQueue<DispatcherOperation>();

        _tlsDispatcher = this; // use TLS for ownership only
        _dispatcherThread = Thread.CurrentThread;

        // Add ourselves to the map of dispatchers to threads.
        lock(_globalLock)
        {
            _dispatchers.Add(new WeakReference(this));
        }

        _unhandledExceptionEventArgs = new DispatcherUnhandledExceptionEventArgs(this);
        _exceptionFilterEventArgs = new DispatcherUnhandledExceptionFilterEventArgs(this);

        _defaultDispatcherSynchronizationContext = new DispatcherSynchronizationContext(this);

        // Create the message-only window we use to receive messages
        // that tell us to process the queue.
        MessageOnlyHwndWrapper window = new MessageOnlyHwndWrapper();
        _window = new SecurityCriticalData<MessageOnlyHwndWrapper>( window );

        _hook = new HwndWrapperHook(WndProcHook);
        _window.Value.AddHook(_hook);

        // DDVSO:447590
        // Verify that the accessibility switches are set prior to any major UI code running.
        AccessibilitySwitches.VerifySwitches(this);
    }

Обновлять

Обновлен код конструктора из .net с открытым исходным кодом. Dispatcher.cs доступен здесь https://referencesource.microsoft.com/#WindowsBase/Base/System/Windows/Threading/Dispatcher.cs,078d6b27d9837a35

После некоторого расследования мы обнаружили, что проблема возникает примерно после 15000 итераций (каждая итерация создает новый поток и вызывает PngBitmapEncoder). Затем выяснилось, что это связано с ограничением Global Atom Table (0x4000 или 16384). Подробнее о Global Atom Table здесь https://docs.microsoft.com/en-us/archive/blogs/ntdebugging/identifying-global-atom-table-leaks

Создаваемый каждый раз диспетчер вносит запись в таблицу Global Atom, и при выходе из потока эта запись не очищается. Это приводит к утечке в таблице Global Atom, и когда она достигает максимального предела, выдается ошибка «Недостаточно памяти....». Это похоже на проблему с обработкой Microsoft Dispatcher. Даже в документации PngBitmapEncoder я не вижу никаких замечаний относительно обработки Dispatcher и какого-либо явного отключения диспетчера.


person Rahul Sundar    schedule 16.08.2020    source источник
comment
Вы показали конструктор static, который будет отображаться как .cctor. Это не конструктор экземпляра, который выбрасывает.   -  person Jon Skeet    schedule 16.08.2020
comment
Кажется, это утечка памяти или? Это неуправляемый код, и вам нужно освободить используемую память, вы уверены, что делаете это?   -  person RoXTar    schedule 16.08.2020
comment
Диспетчеры создаются по одному на поток и создают собственное окно для получения сообщений Windows. Они хранятся в слабой ссылке, поэтому они будут завершены/удалены, когда вы больше не будете на них ссылаться. Вы должны каким-то образом удерживать DispatcherObject (который BitmapEncoder является DispatcherObject), поэтому они никогда не освобождают Dispatcher даже после того, как поток умирает.   -  person Bryce Wagner    schedule 16.08.2020
comment
Неправильный класс, неправильный конструктор, ошибка HwndWrapper. Вероятно, вы можете диагностировать эту проблему с помощью диспетчера задач, добавив столбец «Пользовательские объекты». Шоу заканчивается, когда оно достигает 10000 для вашего процесса и 65535 для всех процессов, работающих на одном рабочем столе. Если это услуга, то применяются гораздо более низкие ограничения.   -  person Hans Passant    schedule 16.08.2020
comment
@JonSkeet Хорошее наблюдение. Код из .Net с открытым исходным кодом взят из .Net 4.8, а стек вызовов — из .Net 4.5.1. Я думал, что код этого конструктора не изменится с версии 4.5.1 до 4.8. Судя по вашему наблюдению, я, вероятно, смотрю на неправильную версию кода.   -  person Rahul Sundar    schedule 16.08.2020
comment
@HansPassant Позвольте мне проверить столбец UserObjects. Кстати, мы используем Windows Server 2008 R2. Так и здесь ограничение в 10000 такое же?   -  person Rahul Sundar    schedule 16.08.2020
comment
@RoXTar Мы не наблюдали заметной тенденции к утечке памяти в течение определенного периода времени (3 дня) и не обрабатывали утечку. Это то, что заставило меня выглядеть загадочным в этой проблеме   -  person Rahul Sundar    schedule 16.08.2020


Ответы (1)


У меня была эта проблема несколько лет назад, когда я выполнял фоновую обработку с использованием объектов в пространстве имен System.Windows.Media.Imaging. Я наткнулся на запись в блоге от инженера Microsoft, который признал, что это была проблема, но не было достаточного интереса к ее решению или что-то в этом роде. Я помню, как надеялся, что они исправят это в ревизиях фреймворка. Инженер опубликовал решение, которое работает для меня.

Я должен упомянуть, что я пытался использовать решение в пуле потоков, используя System.Threading.ThreadPool.QueueUserWorkItem() или System.Threading.Tasks.Task.Run(), но обнаружил, что решение не работает в пуле потоков; возможно, потому что нить используется повторно. Единственный способ, которым я смог решить эту проблему, — это использовать System.Threading.Thread для выполнения этой работы. Ниже приведена основная идея о том, как обойти проблему и принудительно освободить ресурс.

new System.Threading.Thread(new System.Threading.ThreadStart(() =>
{
    // Do some imaging work.

    // This asks the dispatcher associated with this thread to shut down right away.
    System.Windows.Threading.Dispatcher.CurrentDispatcher.BeginInvokeShutdown(System.Windows.Threading.DispatcherPriority.Normal);
    System.Windows.Threading.Dispatcher.Run();
})).Start();
person Rich    schedule 28.08.2020
comment
Утешительно видеть, что я не один столкнулся с этой проблемой. По поводу решения у меня 2 вопроса. Q1) Могу ли я использовать InvokeShutdown() для синхронного вызова. Какова стоимость InvokeShutdown(), займет ли это больше времени или заблокирует что-то на время? Q2) Почему нам нужно снова запускать диспетчер после завершения работы? - person Rahul Sundar; 28.08.2020
comment
Все это разочаровывает, что они еще не исправили это, у меня была эта проблема много лет назад. Решение кажется мне взломом, потому что у меня те же вопросы, что и у вас. Я полагаю, вы могли бы попробовать InvokeShutdown(), но если ваш процесс похож на мой, потребуется несколько дней, чтобы определить, работает ли изменение. Те две строки кода, о которых написал инженер в своем блоге, определенно решают проблему как есть. Начните с этих строк и убедитесь, что это решает вашу проблему. Тогда, если вы хотите поработать с этими линиями и получить хорошие результаты, дайте мне знать. Как я уже сказал, я возился с пулом потоков, и это не сработало. - person Rich; 28.08.2020
comment
Спасибо, что поделились своими результатами. У меня работают как BeginInvokeShutdown(), так и InvokeShutdown(). Позвольте мне победить это, работая в течение нескольких недель, и я обновлю, если появятся какие-либо интересные результаты. Спасибо еще раз - person Rahul Sundar; 29.08.2020