Как следует управлять очисткой таймеров, объявленных внутри области действия функции?

В следующем коде Timer объявляется внутри функции, где она также подписывается на событие Elapsed:

    void StartTimer()
    {
        System.Timers.Timer timer = new System.Timers.Timer(1000);
        timer.Elapsed += new System.Timers.ElapsedEventHandler(timer_Elapsed);
        timer.AutoReset = false;
        timer.Start();
    }

Как только функция завершается, ссылка на Timer теряется (я полагаю).

Отменяется ли регистрация события Elapsed автоматически при уничтожении объекта, или, если нет, может ли событие быть отменено в самом событии Elapsed:

    void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
    {
        var timer = (System.Timers.Timer)sender;
        timer.Elapsed -= timer_Elapsed;
    }

Будет ли этот подход по-прежнему позволять правильную очистку объекта, или по этой причине таймеры никогда не должны объявляться в локальной области действия функции?


person Alfie    schedule 18.07.2014    source источник


Ответы (2)


Правила здесь запутаны, вы не можете видеть, что происходит внутри CLR. Который поддерживает список активных таймеров, System.Timers.Timer имеет ссылку в этом списке, которая поддерживает его в рабочем состоянии и предотвращает сборку мусора. Необходимо в вашем случае, поскольку вашей локальной переменной в вашем методе StartTimer() недостаточно, чтобы поддерживать ее в рабочем состоянии.

Если AutoReset = false, CLR удаляет таймер из списка, когда он тикает. Осталась единственная ссылка на аргумент sender в обработчике события Elapsed.

Если вы не активируете таймер явным образом с помощью sender, тем самым возвращая его в очередь CLR, то ссылка на объект Timer не остается. Он будет собираться мусором всякий раз, когда запускается сборщик мусора.

Отмена подписки на обработчик событий Elapsed не влияет на это. Это еще одна деталь, которую очень трудно увидеть, ваша подписка на мероприятие добавила ссылку на это. Другими словами, объект Timer фактически поддерживает ваш внешний объект в рабочем состоянии. Что, конечно, хорошо, вы бы не хотели, чтобы ваш объект собирал мусор, пока таймер все еще может вызывать ваш обработчик событий Elapsed. Если вы не хотите, чтобы время жизни объекта не продлевалось таймером, вам придется проделать больше работы. Теперь необходимо явно отписать обработчик события и остановить таймер. Что требует от вас сохранения ссылки на объект Timer.

Также имейте в виду, что если ваш класс реализует сам IDisposable, он также должен удалять Timer. Необходимо, потому что обычно вы не хотите, чтобы обработчик событий Elapsed запускался для удаленного объекта, который имеет тенденцию вызывать ObjectDisposedExceptions. Снова причина хранить ссылку на объект Timer в поле вашего класса. Остерегайтесь очень неприятной ошибки гонки потоков, которая скрыта под напольным ковриком, событие Elapsed все еще может выполняться после или пока вы вызываете метод таймера Dispose(). Блокировка необходима, чтобы предотвратить сбой вашей программы раз в год или месяц с голубой луной. В остальном это не отличается от обычных мер предосторожности, которые вы должны предпринять, когда разрешаете выполнение кода в рабочем потоке и доступ к общему состоянию.

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

person Hans Passant    schedule 18.07.2014

Просто утилизируйте таймер:

void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
    var timer = (System.Timers.Timer)sender;
    timer.Dispose(); //here
}

Отключение события не требуется, поскольку удаление по соглашению всегда является полным удалением.

Кстати, все, что вы там написали, эквивалентно:

await Task.Delay(1000);
person usr    schedule 18.07.2014