Утечка ресурсов при создании замораживаемых объектов в фоновом потоке
В моем приложении я создаю 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 ...
Я думаю, что это происходит так:
DrawingGroup
происходит отDependencyObject
, а такжеDependencyObject
конструктор используетDispatcher.CurrentDispatcher
, который затем создает новыйDispatcher
для этой темы.- Новый диспетчер выделяет некоторый ресурс Win32.
- Смотря на
HwndWrapper
Доработка кода в Reflector, я думаюHwndWrapper
пытается синхронизировать свою собственную очистку, используяDispatcher.BeginInvoke
, - Поскольку этот фоновый поток никогда не запускает цикл сообщений, код очистки никогда не будет вызван => утечка ресурсов
Есть ли способ решить или обойти эту проблему?
Что я уже пробовал:
- Очевидно, что используя
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();
}
}