Утечка ресурсов при создании замораживаемых объектов в фоновом потоке

В моем приложении я создаю Freezable объекты в фоновых потоках (пул потоков), замораживают их и затем отображают в основном потоке. Все работает, за исключением того, что через некоторое время вся система становится вялой, и приложение в конечном итоге падает.

Мне удалось уменьшить проблему до этой линии:

var temp = new DrawingGroup();

Если вы выполняете это достаточно часто в разных фоновых потоках (не в пользовательском интерфейсе), вся система становится вялой, и в конечном итоге происходит сбой приложения.

(В моем реальном приложении я затем рисую что-то для этого объекта, замораживаю его и затем отображаю в главном потоке, но это не является необходимым для воспроизведения проблемы.)

Полный код для воспроизведения этой проблемы (скопируйте в пустое wpf-приложение по умолчанию):

public partial class MainWindow : Window
{
    private DispatcherTimer dt;

    public MainWindow()
    {
        InitializeComponent();

        dt = new DispatcherTimer();
        dt.Interval = TimeSpan.FromSeconds(0.1);
        dt.Tick += dt_Tick;
        dt.IsEnabled = true;
    }

    private int counter = 0;
    void dt_Tick(object sender, EventArgs e)
    {
        for (int i = 0; i < 100; i++)
        {
            var thread = new Thread(MemoryLeakTest);
            thread.Start();
        }

        Title = string.Format("Mem leak test {0}", counter++);

    }

    private void MemoryLeakTest()
    {
        try
        {
            var temp = new DrawingGroup();
            temp.Freeze();
        }
        catch (Exception e)
        {
            dt.IsEnabled = false;
            MessageBox.Show(e.Message+Environment.NewLine+e.StackTrace);
        }
    }
}

После ~150 запусков таймера (т.е. после того, как за короткое время было создано около 15000 потоков), я получаю следующее исключение:

Not enough storage is available to process this command
   bei MS.Win32.HwndWrapper..ctor(Int32 classStyle, Int32 style, Int32 exStyle, Int32 x, Int32 y, Int32 width, Int32 height, String name, IntPtr parent, HwndWrapperHook[] hooks)
   bei System.Windows.Threading.Dispatcher..ctor()
   bei System.Windows.DependencyObject..ctor()
   bei System.Windows.Media.DrawingGroup..ctor()
   bei WpfApplication5.MainWindow.MemoryLeakTest() in ...

Я думаю, что это происходит так:

  1. DrawingGroup происходит от DependencyObject, а также DependencyObjectконструктор использует Dispatcher.CurrentDispatcher, который затем создает новый Dispatcher для этой темы.
  2. Новый диспетчер выделяет некоторый ресурс Win32.
  3. Смотря на HwndWrapperДоработка кода в Reflector, я думаю HwndWrapper пытается синхронизировать свою собственную очистку, используя Dispatcher.BeginInvoke,
  4. Поскольку этот фоновый поток никогда не запускает цикл сообщений, код очистки никогда не будет вызван => утечка ресурсов

Есть ли способ решить или обойти эту проблему?

Что я уже пробовал:

  • Очевидно, что используя ThreadPool или же Tasks вместо того, чтобы создавать темы вручную, эта проблема затягивается. Но ThreadPool со временем также создает и закрывает новые потоки, так что это только задерживает проблему, но не является решением.
  • Принудительный сбор GC в конце каждого потока ничего не изменил. Это не про индетерминизм сбора мусора.
  • призвание Dispatcher.InvokeShutdown вручную в конце фонового потока, кажется, работает, но я не вижу, как я мог гарантировать, что он вызывается в конце каждого ThreadPool нить. Без написания своего ThreadPool, то есть...

2 ответа

Решение

Это известный дефект конструкции системы Диспетчер в.NET. Это влияет на библиотеки WPF и не-WPF, которые используют Dispatcher. Microsoft указала, что это не будет исправлено.

Он не связан с использованием операции замораживания или какой-либо другой операции. Любой объект (класс), производный от DependencyObject, будет иметь базовый конструктор, который запускает создание Dispatcher экземпляр для этого потока, если он не был создан ранее. Другими словами, Dispatcher является локальным потоком синглтона по дизайну.

Авария происходит, когда достаточно (десятки тысяч) случаев Dispatcher были утечки. Это означает, что за время жизни приложения было создано и уничтожено одинаковое количество потоков, каждый из которых создал один или несколько DependencyObject "S. Спросите любого разработчика приложения, и он скажет, что это будет необычно, хотя само по себе неплохо, но определенно нуждается в особом внимании при разработке приложения, в котором будет создано и уничтожено много потоков.


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

Thread currentThread = Thread.CurrentThread;
Dispatcher currentDispatcherOrNull = Dispatcher.FromThread(currentThread);

MSDN: Dispatcher.FromThread метод


Во-первых, вы можете выключить диспетчер после того, как закончите с потоком.

MSDN: Dispatcher.InvokeShutdown метод


Во-вторых, осознайте, что после того, как он был закрыт для одного потока, невозможно повторно инициализировать Dispatcher для той же темы. Другими словами, после InvokeShutdown, невозможно использовать WPF или любую другую библиотеку, зависящую от Dispatcher в этой теме. Нить эффективно отравлена ​​до смерти.


Объединение первого и второго пунктов приводит к выводу, что вам нужен собственный пул потоков, каждый из которых наделен Dispatcher, Пока вы контролируете сворачивание пула нитей, опасности утечки нет.


Существуют популярные библиотеки пулов потоков.NET с открытым исходным кодом, которые могут работать вместе (независимо от) пула системных потоков.NET. Это подходящий способ решения этой конкретной проблемы с платформой.


Если вы контролируете как внешний интерфейс (уровень представления), так и внутренний (отображение изображений), существует более простой, драконовский и эффективный (хотя и недоиспользуемый) подход:

  • Делает это политикой, что вызывающая сторона должна инициализировать Dispatcher; серверная часть просто проверит, что диспетчер уже существует (через Dispatcher.FromThread) и отказаться от работы, если это не так.

Этот подход переносит нагрузку на уровень представления, который по иронии судьбы обычно инициализирует Dispatcher.

Этот подход также применим для пула потоков одного.

Я прав, что вы используете эту логику с TPL или же ThreadPool?
Если это так, и последний вариант является вариантом для вас, вы можете легко получить DrawingGroup свойство Dispatcher, и вызвать его InvokeShutdown метод в finally блок.

Таким образом, вы можете написать что-то вроде этого:

DrawingGroup temp;
try
{
    temp = new DrawingGroup();
}
finally
{
    // do the work
    if (temp != null)
    {
        temp.Dispatcher.InvokeShutdown();
    }
}
Другие вопросы по тегам