WedX - журнал о программировании и компьютерных науках

Утечка памяти, если таймер не останавливается явно

Какие ссылки создает экземпляр System.Timers.Timer в моем приложении, если я создаю/подписываюсь и запускаю его?

Вот ссылки, о которых я знаю:

  1. Ссылка на экземпляр нашего таймера, который мы храним в некоторой переменной.
  2. Неявная ссылка, когда мы подписываемся на событие Elapsed

Я предполагаю, что должны быть созданы какие-то дополнительные ссылки, когда мы Start запускаем наш таймер. Но что это такое?

Вот фрагмент кода, с которым я экспериментировал:

class Program
{
    static void Main(string[] args)
    {
        var timers = new List<Timer>();

        Console.WriteLine("Timers to create/start and subscribe: ");

        // step 1
        var count = 1000000;
        for (int i = 0; i < count; i++)
        {
            var timer = new Timer(100000);
            timers.Add(timer);

            // step 2
            timer.Elapsed += Callback;
            timer.Start();
        }

        // step 3: 7mb -> 240mb
        Console.WriteLine("All timers subscribed and started");

        Task.Delay(TimeSpan.FromSeconds(15)).Wait();

        // step 4
        timers.ForEach(t => t.Elapsed -= Callback);
        Console.WriteLine("All timers unsubscribed");

        // step 5: uncomment next two lines to have timers GC collected.
        // timers.ForEach(t => t.Stop());
        // Console.WriteLine("All timers stopped");

        // step 6
        timers.Clear();
        Console.WriteLine("Collection cleared");

        // step 7
        do
        {
            Console.WriteLine("Press Enter to GC.Collect");
            Console.ReadLine();
            GC.Collect();
            Console.WriteLine("GC Collected");
        }
        while (true);
    }

    private static void Callback(object sender, ElapsedEventArgs e)
    {
        Console.WriteLine("callback");
    }
}

Вот что я делаю здесь:

  1. Создайте миллион экземпляров Timer и сохраните их в списке.
  2. Для каждого таймера: подпишитесь на событие Elapsed, передав ему ссылку на статический метод, а затем вызовите Start.
  3. Как только это будет сделано, объем памяти моего приложения увеличится с 7 МБ до 240 МБ.
  4. После этого я отписываюсь от всех событий Elapsed таймеров, тем самым убивая неявную ссылку на подписку на события.
  5. НЕОБЯЗАТЕЛЬНО: Остановите все таймеры.
  6. Очистить список таймеров, чтобы удалить ссылки на их экземпляры.
  7. GC.Collect для освобождения памяти, выделенной для таймеров.

Если выполняется Step 5 (таймеры остановлены), то на GC.Collect шаге я вижу, что память, занимаемая моим приложением, уменьшается с 240 МБ до примерно 20 МБ, что означает, что таймеры были перезапущены правильно.

Если Step 5 опущено, то на GC.Collect шаге использование памяти остается на уровне 240 МБ независимо от того, как долго я жду и принудительно собираю сборщик мусора. Что фактически означает, что экземпляры таймера остаются в памяти и не могут быть собраны.

Из того, что я знаю, как работает GC, мои таймеры должны быть очищены из памяти, если они недоступны из корня (имеется в виду отсутствие ссылок).

Есть ли другие ссылки на мои таймеры, которые я могу удалить без явного вызова timer.Stop()?

Есть ли другой способ предотвратить утечку памяти, не вызывая метод Stop?


  • Убедитесь, что вы делаете это в сборке релиза, или выделите создание списка таймеров в отдельный метод. Если вы запускаете отладочную сборку или подключаете отладчик, многие переменные изменяют свою жизнь для обработки отладки и проверки переменных. 06.11.2017
  • вы действительно создаете 100k таймеров? 06.11.2017
  • System.Timers.Timer — довольно ужасный класс, из-за него у многих программистов возникают проблемы. Но, скорее всего, был добавлен в .NET 1.0, поскольку они опасались, что у них возникнут дополнительные проблемы с System.Threading.Timer. У которого есть довольно нежелательное свойство, он получает сборщик мусора, если программа не сохраняет явную ссылку на него. Сохранение его в локальной переменной — обычная ошибка. Timers.Timer продолжает тикать, даже если в программе нет ссылки на него. Так что конечно, шаг 5 имеет существенное значение. 06.11.2017
  • System.Timers.Timer это IDisposable. когда вы используете экземпляры классов, которые реализуют интерфейс IDisposable, вы должны удалить их. 06.11.2017
  • @ZoharPeled Если тип реализует IDisposable, это означает, что я должен его удалять, а не должен. Если я забуду вызвать Dispose, то у меня может быть deferred коллекция экземпляра gc, но это не должно быть утечкой памяти, что означает, что вы не сможете восстановить эту память. 06.11.2017
  • Даже если сборщик мусора соберет объекты вашего таймера, он не вызовет Dispose() вместо вас. 06.11.2017
  • @steavy, если IDisposable использует неуправляемые ресурсы, он освобождает их только при вызове Dispose. см. здесь — вы реализуете метод Dispose для освобождения неуправляемых ресурсов, используемых вашим приложением. Сборщик мусора .NET не выделяет и не освобождает неуправляемую память. 06.11.2017
  • @ZoharPeled Если вы используете неуправляемые ресурсы, рекомендуется реализовать финализатор и Dispose. Это то, чего я ожидал от Timer, если бы он использовал неуправляемые ресурсы. 06.11.2017
  • @LasseVågsætherKarlsen Да, проверил это в режиме выпуска без подключенного отладчика. 06.11.2017
  • @HansPassant Как я могу не хранить его в переменной? Мне нужна ссылка, чтобы управлять им. Так это было сделано по замыслу. Странное решение, как по мне, гораздо проще обнаружить не тикающий таймер, чем утечку памяти. 06.11.2017
  • Вы можете прочитать этот вопрос, который должен пролить свет на вашу проблему: stackoverflow.com/q/4962172/5311735 06.11.2017
  • Не уверен, что ты пытаешься сказать. Вам просто не нужно хранить его вообще, обработчик события Elapsed возвращает ссылку через аргумент sender. Его поведение принципиально не отличается, скажем, от класса Form. Пока окно формы отображается на экране, базовый объект формы не может быть удален сборщиком мусора. То же самое и с Таймером, пока он не остановлен или не утилизирован, он будет продолжать жить и тикать. 06.11.2017
  • @steavy на самом деле Microsoft рекомендует использовать SafeHandle, а не Finalize. Но это не имеет отношения к данному обсуждению. Моя точка зрения заключалась в том, что если класс реализует метод IDisposable, вам необходимо его удалить. Вам все равно, даже если метод Dispose ничего не делает - здесь важно то, что он реализует интерфейс IDisposable. 06.11.2017
  • @ZoharPeled Спасибо за ссылку, буду читать. Я не говорю, что можно пренебречь вызовом Dispose. Я просто думаю, что если вы забудете его вызвать, это не должно вызывать постоянной утечки памяти, пока мы говорим об управляемых объектах. Видимо Timer было задумано иначе. 07.11.2017
  • Как вы можете понять из документации, в отличие от класса System.Threading.Timer, который требует, чтобы вызывающий код сохранял ссылку на объект, класс System.Timers.Timer имеет собственную внутреннюю ссылку, которая обеспечивает доступность объекта во время его выполнения. Поведение, которое вы видите, совершенно нормальное, ожидаемое и достаточно хорошо задокументированное. См. отмеченный дубликат для дальнейшего обсуждения этой темы. 07.11.2017

Новые материалы

Объяснение документов 02: BERT
BERT представил двухступенчатую структуру обучения: предварительное обучение и тонкая настройка. Во время предварительного обучения модель обучается на неразмеченных данных с помощью..

Как проанализировать работу вашего классификатора?
Не всегда просто знать, какие показатели использовать С развитием глубокого обучения все больше и больше людей учатся обучать свой первый классификатор. Но как только вы закончите..

Работа с цепями Маркова, часть 4 (Машинное обучение)
Нелинейные цепи Маркова с агрегатором и их приложения (arXiv) Автор : Бар Лайт Аннотация: Изучаются свойства подкласса случайных процессов, называемых дискретными нелинейными цепями Маркова..

Crazy Laravel Livewire упростил мне создание электронной коммерции (панель администратора и API) [Часть 3]
Как вы сегодня, ребята? В этой части мы создадим CRUD для данных о продукте. Думаю, в этой части я не буду слишком много делиться теорией, но чаще буду делиться своим кодом. Потому что..

Использование машинного обучения и Python для классификации 1000 сезонов новичков MLB Hitter
Чему может научиться машина, глядя на сезоны новичков 1000 игроков MLB? Это то, что исследует это приложение. В этом процессе мы будем использовать неконтролируемое обучение, чтобы..

Учебные заметки: создание моего первого пакета Node.js
Это мои обучающие заметки, когда я научился создавать свой самый первый пакет Node.js, распространяемый через npm. Оглавление Глоссарий I. Новый пакет 1.1 советы по инициализации..

Забудьте о Matplotlib: улучшите визуализацию данных с помощью умопомрачительных функций Seaborn!
Примечание. Эта запись в блоге предполагает базовое знакомство с Python и концепциями анализа данных. Привет, энтузиасты данных! Добро пожаловать в мой блог, где я расскажу о невероятных..


Для любых предложений по сайту: [email protected]