Подождите, пока вызовы System.Threading.Timer завершатся до выхода из программы

У меня есть List<System.Threading.Timer>. Каждый таймер запускается с настраиваемым интервалом (по умолчанию 10 минут). Все вызовы используют один и тот же метод обратного вызова (с другим параметром). Метод обратного вызова может занять несколько секунд, чтобы завершить работу.

Когда программа завершается, похоже, что выполнение метода обратного вызова немедленно прекращается (я вижу это правильно?).

Как я могу элегантно дождаться завершения всех исполняемых методов обратного вызова до выхода из программы?

Ответ 1

Вы можете отключить все таймеры с параметром WaitHandler. Этот обработчик будет сигнализироваться только тогда, когда метод обратного вызова будет завершен (как указано в spec: "Таймер не расположен до тех пор, пока все завершенные в настоящий момент обратные вызовы не будут завершены".)

void WaitUntilCompleted(List<Timer> myTimers)
{
    List<WaitHandle> waitHnd = new List<WaitHandle>();
    foreach (var timer in myTimers)
    {
        WaitHandle h = new AutoResetEvent(false);
        if(!timer.Dispose(h)) throw new Exception("Timer already disposed.");
        waitHnd.Add(h);
    }
    WaitHandle.WaitAll(waitHnd.ToArray());
}

Изменить: @Peter подчеркнула важность возвращаемого значения метода Dispose. Он возвращает false, когда таймер уже установлен. Чтобы убедиться, что это решение остается надежным, я изменил его, чтобы исключить исключение, когда Timer уже настроен, поскольку мы не можем контролировать в таком случае, когда его обратный вызов заканчивается, несмотря на то, что предыдущий вызов обратного вызова еще может быть запущен!

Ответ 2

Принятый ответ от Томека приятный, но неполный. Если функция Dispose возвращает false, это означает, что нет необходимости ждать завершения, так как поток уже завершен. Если вы попытаетесь подождать WaitHandle в таком случае, WaitAll никогда не вернется, поэтому вы создали себе функцию, которая произвольно зависает вашего приложения/потока.

Вот как это должно выглядеть:

    void WaitUntilCompleted(List<Timer> myTimers)
    {
        List<WaitHandle> waitHnd = new List<WaitHandle>();
        foreach (var timer in myTimers)
        {
            WaitHandle h = new AutoResetEvent(false);
            if (timer.Dispose(h))
            {
                waitHnd.Add(h);
            }
        }
        WaitHandle.WaitAll(waitHnd.ToArray());
    }

Ответ 3

Вы можете использовать ManualResetEvents, чтобы заблокировать основной поток до завершения всех ожидающих операций.

например, если вы хотите, чтобы все таймеры выполнялись хотя бы один раз, тогда у вас может быть массив System.Threading.ManualResetEvent[] с исходным состоянием, установленным на несигнальный

Итак, где-то в вашем коде у вас будет настройка таймера, и это связано с инициализацией waithandle.

// in main setup method.. 
int frequencyInMs = 600000; //10 mins 
Timer timer = new Timer();
timer.Elapsed += (s, e) => MyExecute();
myTimers.Add(timer) 

ManualResetEvent[] _waithandles = new ManualResetEvent[10];
_waithandles[0] = new ManualResetEvent(false);

// Other timers ... 
timer = new Timer();
timer.Elapsed += (s, e) => MyOtherExecute();
myTimers.Add(timer)         
_waithandles[1] = new ManualResetEvent(false);
// etc, and so on for all timers 

// then in each method that gets executed by the timer
// simply set ManualReset event to signalled that will unblock it. 
private void MyExecute() 
{
    // do all my logic then when done signal the manual reset event 
    _waithandles[0].Set(); 
}

// In your main before exiting, this will cause the main thread to wait
// until all ManualResetEvents are set to signalled  
WaitHandle.WaitAll(_waithandles);    

Если вы только хотите дождаться завершения отложенных операций, просто перейдите к следующему:

_waithandles[0] = new ManualResetEvent(true); // initial state set to non blocking. 

private void MyExecute() 
{
    _waithandles[0].Reset(); // set this waithandle to block.. 

    // do all my logic then when done signal the manual reset event 
    _waithandles[0].Set(); 
}

Ответ 4

Вероятно, не удастся дождаться выхода в консольном приложении.

Для приложений форм Windows:

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

И затем вы можете проверить соответствующее событие и ждать, пока счетчик станет 0 или просто отменит выход.