Выполнение кода по расписанию, и пропуск задачи

Есть код для запуска задач по расписанию:

public class TaskScheduler
    {
        private static TaskScheduler _instance;
        private List<Timer> _timers = new List<Timer>();
        private TaskScheduler() { }
        
        public static TaskScheduler Instance => _instance ?? (_instance = new TaskScheduler());
        
        /// <summary>
        /// Create new Task
        /// </summary>
        /// <code>
        /// TaskScheduler.Instance.ScheduleTask(
        /// ()=>
        /// {
        /// 
        /// }, new TimeSpan(0, 20, 0));
        ///
        /// TaskScheduler.Instance.ScheduleTask(Action, TimeSpan);
        /// 
        /// </code>
        /// <param name="task">Action</param>
        /// <param name="time"></param>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="ArgumentOutOfRangeException"></exception>
        /// <exception cref="OverflowException"></exception>
        public void ScheduleTask(Action task, TimeSpan time)
        {
            ScheduleTask(task, time.Hours, time.Minutes, time.Seconds);
        }
        
        /// <summary>
        /// Create new Task
        /// </summary>
        /// <code>
        /// TaskScheduler.Instance.ScheduleTask(
        /// ()=>
        /// {
        /// 
        /// }, 1, 20, 0);
        ///
        /// TaskScheduler.Instance.ScheduleTask(Action, hour, minute, second);
        /// 
        /// </code>
        /// <param name="task">Action</param>
        /// <param name="hour">Hour</param>
        /// <param name="min">Minute</param>
        /// <param name="second">Second</param>
        /// <param name="intervalInHour">Interval in hours</param>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="ArgumentOutOfRangeException"></exception>
        /// <exception cref="OverflowException"></exception>
        public void ScheduleTask(Action task, int hour = 0, int min = 0, int second = 1,  double intervalInHour = 24d)
        {
            var now = DateTime.Now;
            var firstRun = new DateTime(now.Year, now.Month, now.Day, hour, min, second);

            if (now > firstRun) firstRun = firstRun.AddDays(1);
           
            var timeToGo = firstRun - now;

            if (timeToGo <= TimeSpan.Zero) timeToGo = TimeSpan.Zero;
         
            var timer = new Timer(x =>
            {
                task.Invoke();
            }, null, timeToGo, TimeSpan.FromHours(intervalInHour));

            _timers.Add(timer);
        }
    }

Работает великолепно, задачи запускаются, работают.

Есть некий нюанс, есть код который копирует директорию в директорию.

public static class FolderCopper
    {
        public static void CopyAll(DirectoryInfo source, DirectoryInfo target, CancellationToken token)
        {
            if (!source.Exists) return;
            if (!target.Exists) target.Create();

            var po = new ParallelOptions
            {
                CancellationToken = token
            };

            Parallel.ForEach(source.GetDirectories(), po, (sourceChildDirectory) => 
            {
                if (po.CancellationToken.IsCancellationRequested)
                {
                    po.CancellationToken.ThrowIfCancellationRequested();
                }
                CopyAll(sourceChildDirectory, new DirectoryInfo(Path.Combine(target.FullName, sourceChildDirectory.Name)), token);
            });

            Parallel.ForEach(source.GetFiles(), po, sourceFile =>
            {
                if (po.CancellationToken.IsCancellationRequested)
                {
                    po.CancellationToken.ThrowIfCancellationRequested();
                }
                var file = new FileInfo(Path.Combine(target.FullName, sourceFile.Name));
                switch (file.Exists)
                {
                    case false:
                        sourceFile.CopyTo(file.FullName);
                        break;
                    case true when file.LastWriteTimeUtc < sourceFile.LastWriteTimeUtc:
                        sourceFile.CopyTo(file.FullName, true);
                        break;
                }
            });
        }
    }

Его необходимо запускать например раз в 20 или в 10 минут.

Вызываю таким образом:

Utils.TaskScheduler.Instance.ScheduleTask(() => {}, 0, 20, 0, 0.33); //Задача, часы, минуты, секунды, интервал в часах 20 минут - 0.33.

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

Как переписать код, чтобы он не запускал ещё 1 экземпляр, если предыдущий не отработал, и просто пропустить выполнение задачи?


Ответы (3 шт):

Автор решения: Blackmeser

Храните рядом с Action флаги состояния (ожидает запуска, в процессе выполнения), и перед вызовом task.Invoke(); проверяйте предыдущее состояние. Например:

public class TaskScheduler
{
  ...
  bool State = false;
}

var timer = new Timer(x =>
   {
     if (State == false)
     {
       State = true;
       task.Invoke();
       State = false;
     }

   }, null, timeToGo, TimeSpan.FromHours(intervalInHour));
→ Ссылка
Автор решения: tym32167

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

public class MyTimedTask
{
    private Timer _timer;
    private Action _toInvoke;
    private TimeSpan _interval;

    public MyTimedTask(Action toInvoke, TimeSpan interval)
    {
        if (toInvoke == null) throw new ArgumentException(nameof(toInvoke));

        this._toInvoke = toInvoke;
        this._interval = interval;
    }

    public void Start()
    {
        this.Stop();
        _timer = new Timer(Callback, null, _interval, Timeout.InfiniteTimeSpan);
    }

    private void Callback(object state)
    {
        try
        {
            Console.WriteLine();
            Console.WriteLine($"{DateTime.UtcNow.ToLongTimeString()} Invoking from timer start");
            this._toInvoke();
            Console.WriteLine($"{DateTime.UtcNow.ToLongTimeString()} Invoking from timer end");
        }
        finally
        {
            // тут можно расчитать время до следующего запуска по вашему алгоритму
            _timer?.Change(_interval, Timeout.InfiniteTimeSpan);
        }
    }

    public void Stop()
    {
        var local = _timer;
        _timer = null;      
        local?.Dispose();       
    }
}

Шедулер может выглядеть как то так

public class MyScheduler
{
    private List<MyTimedTask> _tasks = new List<MyTimedTask>();

    public void Schedule(Action action, TimeSpan interval)
    {
        var task = new MyTimedTask(action, interval);
        _tasks.Add(task);
        task.Start();
    }

    public void Stop()
    {
        _tasks.ForEach(t => t.Stop());
        _tasks.Clear();
    }
}

Проверка (тут интервал 2 секунды, а задача занимает 5)

void Main()
{
    var scheduler = new MyScheduler();
    scheduler.Schedule(SomeTask, TimeSpan.FromSeconds(2));

    Console.ReadLine();

    scheduler.Stop();
}

private void SomeTask()
{
    Console.WriteLine($"{DateTime.UtcNow.ToLongTimeString()} Invoking SomeTask start");
    Thread.Sleep(5000);
    Console.WriteLine($"{DateTime.UtcNow.ToLongTimeString()} Invoking SomeTask end");
}

Результат

7:11:23 AM Invoking from timer start
7:11:23 AM Invoking SomeTask start
7:11:28 AM Invoking SomeTask end
7:11:28 AM Invoking from timer end

7:11:30 AM Invoking from timer start
7:11:30 AM Invoking SomeTask start
7:11:35 AM Invoking SomeTask end
7:11:35 AM Invoking from timer end

7:11:37 AM Invoking from timer start
7:11:37 AM Invoking SomeTask start
7:11:42 AM Invoking SomeTask end
7:11:42 AM Invoking from timer end

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

→ Ссылка
Автор решения: tym32167

Альтернатива моему первому ответу, мы может переписать код класса, который запускает задачу, на использование монитора и его попытку перезахвата. Код я тупо скопипастил с документации

public class MyTimedTask2
{
    private Timer _timer;
    private Action _toInvoke;
    private TimeSpan _interval;
    
    private object _locker = new Object();

    public MyTimedTask2(Action toInvoke, TimeSpan interval)
    {
        if (toInvoke == null) throw new ArgumentException(nameof(toInvoke));

        this._toInvoke = toInvoke;
        this._interval = interval;
    }

    public void Start()
    {
        this.Stop();
        _timer = new Timer(Callback, null, _interval, _interval);
    }

    private void Callback(object state)
    {
        var timeout = TimeSpan.FromMilliseconds(500);
        
        Console.WriteLine($"{DateTime.UtcNow.ToLongTimeString()} TryEnter attempt");
        if (Monitor.TryEnter(_locker, timeout))
        {
            try
            {
                Console.WriteLine($"{DateTime.UtcNow.ToLongTimeString()} TryEnter sucessed");
                Console.WriteLine($"{DateTime.UtcNow.ToLongTimeString()} Invoking from timer start");
                this._toInvoke();
                Console.WriteLine($"{DateTime.UtcNow.ToLongTimeString()} Invoking from timer end");

            }
            finally
            {               
                Monitor.Exit(_locker);
            }
        }
        else
        {
            Console.WriteLine($"{DateTime.UtcNow.ToLongTimeString()} TryEnter failed");
        }       
    }

    public void Stop()
    {
        var local = _timer;
        _timer = null;

        local?.Dispose();
    }
}

осталось только поправить шедулер

public class MyScheduler
{
    private List<MyTimedTask2> _tasks = new List<MyTimedTask2>();

    public void Schedule(Action action, TimeSpan interval)
    {
        var task = new MyTimedTask2(action, interval);
        _tasks.Add(task);
        task.Start();
    }

    public void Stop()
    {
        _tasks.ForEach(t => t.Stop());
        _tasks.Clear();
    }
}

Код проверки идентичен

void Main()
{
    var scheduler = new MyScheduler();
    scheduler.Schedule(SomeTask, TimeSpan.FromSeconds(2));

    Console.ReadLine();

    scheduler.Stop();
}

private void SomeTask()
{
    Console.WriteLine($"{DateTime.UtcNow.ToLongTimeString()} Invoking SomeTask start");
    Thread.Sleep(5000);
    Console.WriteLine($"{DateTime.UtcNow.ToLongTimeString()} Invoking SomeTask end");
}

В выводе можно видеть удачные и неудачные попытки захвата монитора

7:22:04 AM TryEnter attempt
7:22:04 AM TryEnter sucessed
7:22:04 AM Invoking from timer start
7:22:04 AM Invoking SomeTask start
7:22:06 AM TryEnter attempt
7:22:07 AM TryEnter failed
7:22:08 AM TryEnter attempt
7:22:09 AM TryEnter failed
7:22:09 AM Invoking SomeTask end
7:22:09 AM Invoking from timer end
7:22:10 AM TryEnter attempt
7:22:10 AM TryEnter sucessed
7:22:10 AM Invoking from timer start
7:22:10 AM Invoking SomeTask start
7:22:12 AM TryEnter attempt
7:22:13 AM TryEnter failed
7:22:14 AM TryEnter attempt
7:22:15 AM TryEnter failed
7:22:15 AM Invoking SomeTask end
7:22:15 AM Invoking from timer end
7:22:16 AM TryEnter attempt
7:22:16 AM TryEnter sucessed
7:22:16 AM Invoking from timer start
7:22:16 AM Invoking SomeTask start
7:22:18 AM TryEnter attempt
7:22:19 AM TryEnter failed
7:22:20 AM TryEnter attempt
7:22:21 AM TryEnter failed
7:22:21 AM Invoking SomeTask end
7:22:21 AM Invoking from timer end
7:22:22 AM TryEnter attempt
7:22:22 AM TryEnter sucessed
7:22:22 AM Invoking from timer start
7:22:22 AM Invoking SomeTask start
7:22:27 AM Invoking SomeTask end
7:22:27 AM Invoking from timer end

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

→ Ссылка