Невозвратный таймер С#

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

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

Чтобы уточнить, если t равна одной секунде, а f() выполняет произвольные длительности, которые я написал ниже, затем:

Step  Operation    Time taken
1     wait         1s
2     f()          0.6s
3     wait         0.4s (because f already took 0.6 seconds)
4     f()          10s
5     wait         0s (we're late)
6     f()          0.3s
7     wait         0.7s (we can disregard the debt from step 4)

Обратите внимание, что характер этого таймера заключается в том, что f() не требуется быть безопасным в отношении повторного входа, и пул потоков размером 1 здесь достаточно.

Ответ 1

Вы можете просто использовать "глобальный" уровень var (или, скорее, общедоступное свойство в том же классе, что и f()), который возвращает true, если f() уже запущен.

Итак, если f() был в классе с именем TimedEvent, первым делом f() будет установлено Running true

Таким образом, ваш таймер срабатывает каждую секунду, а затем запускает синхронизированное событие, если оно еще не запущено. if (!timedEvent.Running) timedEvent.f()

Вы прокомментировали, что f() не будет повторяться немедленно, если потребуется больше времени, чем интервал таймера. Это справедливый вопрос. Вероятно, я бы включил такую ​​логику внутри f(), чтобы Running оставался истинным. Поэтому он выглядит примерно так:

public void f(int t) // t is interval in seconds
{
   this.running = true;

   Stopwatch stopWatch = new Stopwatch();
   stopWatch.Start();

   do
   {
       stopwatch.Reset();

       // Do work here

   } while (stopWatch.Elapsed.Seconds > t); // repeat if f() took longer than t

   this.running = false;   
}

Ответ 2

Используйте System.Threading.Timer. Инициализируйте его с периодом тайм-аута. Неопределенный, поэтому он действует как таймер с одним выстрелом. По завершении f() вызовите метод Change(), чтобы перезагрузить его снова.

Ответ 3

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

Обратите внимание, что это приведет к тому, что время немного отличается от того, о чем вы просите. (Между вызовами всегда будет пробел t)

Вы можете решить это, установив интервал на lastTick + t - Now и немедленно запустив метод, если это <= 0.

Остерегайтесь условий гонки, если вам нужно остановить таймер.

Ответ 4

Вы не можете получить таймер, чтобы звонить вам с точно запланированными интервалами. Все таймеры - это вызывать вас не раньше, чем запрошенное время.

Некоторые таймеры лучше других (например, Windows.Forms.Timer очень неустойчив и ненадежен по сравнению с System.Threading.Timer)

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

Чтобы поддерживать время относительно относительно равномерно между этими вызовами, вы можете записать время с момента последнего выполнения обработчиком и использовать его для расчета задержки до следующего события. например Если вы хотите, чтобы вас вызывали один раз в секунду, а ваш таймер завершал обработку в 1.02 с, вы можете настроить следующий обратный вызов таймера в течение 0.98 секунд, чтобы разместить тот факт, что вы уже "использовали" часть следующего второй во время обработки.

Ответ 5

Простое решение:

private class Worker : IDisposable
{
    private readonly TimeSpan _interval;
    private WorkerContext _workerContext;

    private sealed class WorkerContext
    {
        private readonly ManualResetEvent _evExit;
        private readonly Thread _thread;
        private readonly TimeSpan _interval;

        public WorkerContext(ParameterizedThreadStart threadProc, TimeSpan interval)
        {
            _evExit = new ManualResetEvent(false);
            _thread = new Thread(threadProc);
            _interval = interval;
        }

        public ManualResetEvent ExitEvent
        {
            get { return _evExit; }
        }

        public TimeSpan Interval
        {
            get { return _interval; }
        }

        public void Run()
        {
            _thread.Start(this);
        }

        public void Stop()
        {
            _evExit.Set();
        }

        public void StopAndWait()
        {
            _evExit.Set();
            _thread.Join();
        }
    }

    ~Worker()
    {
        Stop();
    }

    public Worker(TimeSpan interval)
    {
        _interval = interval;
    }

    public TimeSpan Interval
    {
        get { return _interval; }
    }

    private void DoWork()
    {
        /* do your work here */
    }

    public void Start()
    {
        var context = new WorkerContext(WorkThreadProc, _interval);
        if(Interlocked.CompareExchange<WorkerContext>(ref _workerContext, context, null) == null)
        {
            context.Run();
        }
        else
        {
            context.ExitEvent.Close();
            throw new InvalidOperationException("Working alredy.");
        }
    }

    public void Stop()
    {
        var context = Interlocked.Exchange<WorkerContext>(ref _workerContext, null);
        if(context != null)
        {
            context.Stop();
        }
    }

    private void WorkThreadProc(object p)
    {
        var context = (WorkerContext)p;
        // you can use whatever time-measurement mechanism you want
        var sw = new System.Diagnostics.Stopwatch();
        int sleep = (int)context.Interval.TotalMilliseconds;
        while(true)
        {
            if(context.ExitEvent.WaitOne(sleep)) break;

            sw.Reset();
            sw.Start();

            DoWork();

            sw.Stop();

            var time = sw.Elapsed;
            if(time < _interval)
                sleep = (int)(_interval - time).TotalMilliseconds;
            else
                sleep = 0;
        }
        context.ExitEvent.Close();
    }

    public void Dispose()
    {
        Stop();
        GC.SuppressFinalize(this);
    }
}

Ответ 6

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

Ответ 7

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

public static Thread StartTimer(TimeSpan interval, Func<bool> operation)
{
    Thread t = new Thread(new ThreadStart(
        delegate()
        {
            DateTime when = DateTime.Now;
            TimeSpan wait = interval;
            while (true)
            {
                Thread.Sleep(wait);
                if (!operation())
                    return;
                DateTime dt = DateTime.Now;
                when += interval;
                while (when < dt)
                    when += interval;
                wait = when - dt;
            }
        }
    ));
    t.IsBackground = true;
    t.Start();
    return t;
}

Ответ 8

В интересах людей, которые приземляются здесь в поисках "повторного входа": (Я знаю, что может быть слишком поздно для исходного вопроса) Если кто-то не прочь использовать библиотеки с открытым исходным кодом, которые уже предоставляют такие функции, я успешно достиг этого с помощью Quartz.NET Когда вы создаете задание и добавляете триггер, вы можете указать, что должно быть сделано, если предыдущий триггер не завершил выполнение задания