Синхронизация метода Timers.Timer Elapsed при остановке

Со ссылкой на эту цитату из MSDN о Система.Таймеры.Таймер:

Событие Timer.Elapsed возникает в потоке ThreadPool, поэтому метод обработки событий может выполняться в одном потоке одновременно с вызовом метода Timer.Stop в другом потоке. Это может привести к возникновению события Elapsed после вызова метода Stop. Это состояние гонки нельзя предотвратить, просто сравнивая свойство SignalTime со временем вызова метода Stop, поскольку метод обработки событий может уже выполняться в момент вызова метода Stop или может начать выполняться между моментом вызова метода Stop. вызывается и момент сохранения времени остановки. Если крайне важно предотвратить выполнение потока, вызывающего метод Stop, пока выполняется метод обработки событий, используйте более надежный механизм синхронизации, например класс Monitor или метод CompareExchange. Код, использующий метод CompareExchange, можно найти в примере для метода Timer.Stop.

Может ли кто-нибудь привести пример «надежного механизма синхронизации, такого как класс Monitor», чтобы объяснить, что именно это означает?

Я думаю, что это означает как-то использовать блокировку, но я не уверен, как вы это реализуете.


person Andy    schedule 26.01.2011    source источник


Ответы (5)


Надежная остановка System.Timers.Timer действительно является серьезной задачей. Самая серьезная проблема заключается в том, что потоки пула потоков, которые он использует для вызова события Elapsed, могут создавать резервные копии из-за алгоритма планировщика пула потоков. Наличие пары резервных вызовов не является чем-то необычным, наличие сотен технически возможно.

Вам понадобятся две синхронизации: одна, чтобы гарантировать, что вы остановите таймер только тогда, когда не запущен обработчик событий Elapsed, другая, чтобы гарантировать, что эти резервные потоки TP не причинят никакого вреда. Так:

    System.Timers.Timer timer = new System.Timers.Timer();
    object locker = new object();
    ManualResetEvent timerDead = new ManualResetEvent(false);

    private void Timer_Elapsed(object sender, ElapsedEventArgs e) {
        lock (locker) {
            if (timerDead.WaitOne(0)) return;
            // etc...
        }
    }

    private void StopTimer() {
        lock (locker) {
            timerDead.Set();
            timer.Stop();
        }
    }

Попробуйте установить для свойства AutoReset значение false. С другой стороны, это хрупко: событие Elapsed вызывается из внутреннего метода .NET, который перехватывает Exception. Очень неприятно, ваш код таймера перестает работать без какой-либо диагностики. Я не знаю истории, но, должно быть, в MSFT была другая команда, которая пыхтела и пыхтела над этим беспорядком и написала System.Threading.Timer. Настоятельно рекомендуется.

person Hans Passant    schedule 26.01.2011
comment
Ничего себе, таймеры действительно коварные звери. :о - person Andy; 27.01.2011
comment
Да, он падает, когда вы летите домой в самолете. Пыхтите и пыхтите, проведите стресс-тест перед посадкой. - person Hans Passant; 27.01.2011
comment
Немного поразмыслив, кажется, что он создает Threading.Timer за кулисами, а обратный вызов действительно поглощает исключения. Реализация базового Threading.Timer сама по себе кажется намного чище, так что я почитаю об этом. - person Andy; 27.01.2011
comment
Привет Ганс. Извините, что комментирую такую ​​старую ветку, но я думаю, что timerDead.WaitOne() на самом деле должно быть timerDead.WaitOne(0). Небольшое (слишком маленькое, чтобы редактировать напрямую), но важное отличие. - person Jason Watkins; 25.07.2013

Это то, что он предлагает.

Monitor — это класс, используемый компилятором C# для заявление lock.

При этом вышеизложенное является проблемой только в том случае, если это проблема в вашей ситуации. Весь оператор в основном переводится как «Вы можете получить событие таймера, которое происходит сразу после вызова Stop(). Если это проблема, вам нужно с ней справиться». В зависимости от того, что делает ваш таймер, это может быть проблемой, а может и нет.

Если это проблема, Timer.Stop показывает надежный способ (с использованием Interlocked.CompareExchange) справиться с этим. Просто скопируйте код из примера и измените при необходимости.

person Reed Copsey    schedule 26.01.2011

Пытаться:

lock(timer) {
timer.Stop();
}
person bbosak    schedule 26.01.2011
comment
Это не будет иметь никакого эффекта, по крайней мере, без совсем другого кода... Кроме того, это будет плохой выбор механизмов синхронизации. - person Reed Copsey; 27.01.2011

Вот очень простой способ предотвратить возникновение этого состояния гонки:

private object _lock = new object();
private Timer _timer; // init somewhere else

public void StopTheTimer()
{
    lock (_lock) 
    {
        _timer.Stop();
    }
}

void elapsed(...)
{
    lock (_lock)
    {
        if (_timer.Enabled) // prevent event after Stop() is called
        {
            // do whatever you do in the timer event
        }
    }
}
person MusiGenesis    schedule 26.01.2011
comment
На самом деле это не предотвратит упомянутое состояние гонки. TimerTick может происходить одновременно с вызовом _timer.Stop() - и все равно срабатывать впоследствии (в этот момент блокировка будет снята). - person Reed Copsey; 27.01.2011
comment
Из процитированного текста я подумал, что цель состояла в том, чтобы предотвратить остановку таймера, если метод обработки событий находился в середине выполнения. Мой код выполнит это (я думаю). - person MusiGenesis; 27.01.2011
comment
Это может привести к возникновению события Elapsed после вызова метода Stop. - это все еще может произойти с этой опцией. - и тело истекшего метода по-прежнему будет выполняться полностью, так как оно не проверяет, остановлен ли таймер... - person Reed Copsey; 27.01.2011
comment
Изменено. Это, вероятно, не лучший способ, но, по крайней мере, он простой. - person MusiGenesis; 27.01.2011

Кажется, таймер не является потокобезопасным. Вы должны синхронизировать все вызовы с ним с помощью блокировки. lock(object){} на самом деле просто сокращение для простого вызова монитора.

person Bengie    schedule 26.01.2011