Ожидание завершения истекшего события таймера до закрытия/остановки приложения/службы

Резюме: В приложении Windows и приложении консоли я звоню в общую библиотеку, которая содержит таймер, который периодически запускает действие, которое занимает около 30 секунд. Это прекрасно работает, однако...

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

Я реализовал эту функциональность, имея свойство Boolean InEvent, которое проверяется при вызове метода остановки таймера.

Пока это функционально, возникает вопрос: является ли это лучшим способом сделать это? Существует ли альтернативный подход, который может служить этой цели лучше?

Другая проблема заключается в том, что мне нужно избегать запроса на остановку службы, если "Служба не ответила на запрос остановки"

Это моя реализация

public sealed class TimedProcess : IDisposable
{
    static TimedProcess singletonInstance;
    bool InEvent;
    Timer processTimer;

    private TimedProcess()
    {
    }

    public static TimedProcess Instance
    {
        get
        {
            if (singletonInstance == null)
            {
                singletonInstance = new TimedProcess();
            }

            return singletonInstance;
        }
    }

    public void Start(double interval)
    {
        this.processTimer = new Timer();
        this.processTimer.AutoReset = false;
        this.processTimer.Interval = interval;
        this.processTimer.Elapsed += new ElapsedEventHandler(this.processTimer_Elapsed);
        this.processTimer.Enabled = true;
    }

    public void Stop()
    {
        if (processTimer != null)
        {
            while (InEvent)
            {
            }

            processTimer.Stop();
        }
    }

    void processTimer_Elapsed(object sender, ElapsedEventArgs e)
    {
        try
        {
            InEvent = true;
            // Do something here that takes ~30 seconds
        }
        catch
        {
        }
        finally
        {
            InEvent = false;
            processTimer.Enabled = true;
        }
    }

    public void Dispose()
    {
        if (processTimer != null)
        {
            Stop();
            processTimer.Dispose();
        }
    }
}

И вот как он вызывается в главном приложении OnStart/console:

TimedProcess.Instance.Start(1000);

Так вызывается в сервисе OnStop и main приложения (ожидает нажатия):

TimedProcess.Instance.Stop();

Ответ 1

Вероятно, самый простой и надежный способ - использовать Monitor. Создайте объект, к которому может обратиться основная программа и обратный вызов таймера:

private object _timerLock = new object();

Ваша основная программа пытается заблокировать это до выключения:

// wait for timer process to stop
Monitor.Enter(_timerLock);
// do shutdown tasks here

И ваш обратный вызов по таймеру также блокирует его:

void processTimer_Elapsed(object sender, ElapsedEventArgs e)
{
    if (!Monitor.TryEnter(_timerLock))
    {
        // something has the lock. Probably shutting down.
        return;
    }
    try
    {
        // Do something here that takes ~30 seconds
    }
    finally
    {
        Monitor.Exit(_timerLock);
    }
}

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

Если вы хотите, чтобы основная программа продолжала работу и закрывалась через некоторое время, независимо от того, получила ли она блокировку, используйте Monitor.TryEnter. Например, это будет ждать 15 секунд.

bool gotLock = Monitor.TryEnter(_timerLock, TimeSpan.FromSeconds(15));

Возвращаемое значение true, если оно удалось получить блокировку.

Кстати, я настоятельно рекомендую использовать System.Threading.Timer, а не System.Timers.Timer. Последний сквозит исключения, которые могут в конечном итоге скрывать ошибки. Если в событии Elapsed возникает исключение, оно никогда не исчезнет, ​​а это значит, что вы никогда не знаете об этом. Дополнительную информацию см. В сообщении в блоге.

Ответ 2

ИЗМЕНИТЬ

Каждый обратный вызов System.Timers.Timer помещается в очередь на ThreadPool. Имейте в виду, что System.Timers.Timer может иметь состояние гонки (вы можете прочитать больше об этом здесь.) System.Threading.Timer - немного более приятная оболочка, которая Я предпочитаю использовать из-за этого простоту.

Вы не описали достаточно подробностей, чтобы узнать, может ли ваше конкретное приложение справиться с этим состоянием гонки, поэтому трудно сказать. Но, учитывая ваш код, возможно, что после < Stop() может возникнуть обратный вызов для processTimer_Elapsed.


Для проблемы с таймаутом службы -

Один из способов сделать это - сделать вызов метода ServiceController WaitForStatus с таймаутом. Я делал это в прошлом, и он работает достаточно хорошо, хотя я помню, что в течение очень долгого времени были случаи с краем.

См. ссылку MSDN. Пример использования здесь.

Ответ 3

Одна из возможных альтернатив, похоже, заключается в том, чтобы не выполнять фактическую работу в самом обратном вызове таймера, а просто чтобы опустить в очередь рабочий элемент из пула протектора для выполнения работы. Затем вы можете пойти и утилизировать таймер - все, что в настоящее время работает в пуле потоков, останется в рабочем состоянии, и ваша служба может немедленно ответить на запрос остановки, но элемент пула потоков (если он поставлен в очередь) все равно будет обработан.